极客时间已完结课程限时免费阅读

17 | 消息队列:秒杀时如何处理每秒上万次的下单请求?

17 | 消息队列:秒杀时如何处理每秒上万次的下单请求?-极客时间

17 | 消息队列:秒杀时如何处理每秒上万次的下单请求?

讲述:唐扬

时长11:46大小10.78M

你好,我是唐扬。
在课程一开始,我就带你了解了高并发系统设计的三个目标:性能、可用性和可扩展性,而在提升系统性能方面我们一直关注的是系统的查询性能,也用了很多的篇幅去讲解数据库的分布式改造,各类缓存的原理和使用技巧。究其原因在于我们遇到的大部分场景都是读多写少,尤其是在一个系统的初级阶段。
比如一个社区的系统初期一定是只有少量的种子用户在生产内容,而大部分的用户都在“围观”别人在说什么。此时,整体的流量比较小,而写流量可能只占整体流量的百分之一,那么即使整体的 QPS 到了 10000 次 / 秒,写请求也只是到了每秒 100 次,如果要对写请求做性能优化,它的性价比确实不太高。
但随着业务发展,你可能会遇到一些存在高并发写请求的场景,其中秒杀抢购就是最典型的场景。假设你的商城策划了一期秒杀活动,活动在第五天的 00:00 开始,仅限前 200 名,那么秒杀即将开始时,后台会显示用户正在疯狂地刷新 APP 或者浏览器来保证自己能够尽量早的看到商品。
这时,你面对的依旧是读请求过高,那么应对的措施有哪些呢?
因为用户查询的是少量的商品数据,属于查询的热点数据,你可以采用缓存策略将请求尽量挡在上层的缓存中,能被静态化的数据(比如商城里的图片和视频数据)尽量做到静态化,这样就可以命中 CDN 节点缓存减少 Web 服务器的查询量和带宽负担。Web 服务器比如 Nginx 可以直接访问分布式缓存节点,从而避免请求到达 Tomcat 等业务服务器。
当然,你可以加上一些限流的策略,比如对短时间之内来自某一个用户、某一个 IP 或者某一台设备的重复请求做丢弃处理。
通过这几种方式,请求就可以尽量挡在数据库之外了。
稍微缓解了读请求之后,00:00 分秒杀活动准时开始,用户瞬间向电商系统请求生成订单,扣减库存,用户的这些写操作都是不经过缓存直达数据库的。1 秒钟之内,有 1 万个数据库连接同时达到,系统的数据库濒临崩溃,寻找能够应对如此高并发的写请求方案迫在眉睫。这时你想到了消息队列。

我所理解的消息队列

你应该已经了解消息队列到底是什么了,所以我不再讲解它的概念,只聊聊自己对消息队列的看法。我在历年的工作经历中,一直把消息队列看作暂时存储数据的一个容器,认为它是一个平衡低速系统和高速系统处理任务时间差的工具,我给你举个形象的例子。
比如古代的臣子经常去朝见皇上陈述一些国家大事,等着皇上拍板做决策。但是大臣很多,如果同时去找皇上,你说一句我说一句,皇上肯定会崩溃。后来变成臣子到了午门之后要原地等着皇上将他们一个一个地召见进大殿商议国事,这样就可以缓解皇上处理事情的压力了。你可以把午门看作一个暂时容纳臣子的容器,也就是我们所说的消息队列。
其实你在一些组件中都会看到消息队列的影子:
在 Java 线程池中我们就会使用一个队列来暂时存储提交的任务,等待有空闲的线程处理这些任务;
操作系统中,中断的下半部分也会使用工作队列来实现延后执行;
我们在实现一个 RPC 框架时,也会将从网络上接收到的请求写到队列里,再启动若干个工作线程来处理。
……
总之,队列是在系统设计时一种常见的组件。
那么我们如何用消息队列解决秒杀场景下的问题呢?接下来,我们结合具体的例子来看看消息队列在秒杀场景下起到的作用。

削去秒杀场景下的峰值写流量

刚才提到,在秒杀场景下短时间之内数据库的写流量会很高,那么依照我们以前的思路应该对数据做分库分表。如果已经做了分库分表,那么就需要扩展更多的数据库来应对更高的写流量。但是无论是分库分表还是扩充更多的数据库都会比较复杂,原因是你需要将数据库中的数据做迁移,这个时间就要按天甚至按周来计算了。
而在秒杀场景下高并发的写请求并不是持续的,也不是经常发生的,而只有在秒杀活动开始后的几秒或者十几秒时间内才会存在。为了应对这十几秒的瞬间写高峰花费几天甚至几周的时间来扩容数据库,再在秒杀之后花费几天的时间来做缩容,这无疑是得不偿失的。
所以我们的思路是:将秒杀请求暂存在消息队列中,然后业务服务器会响应用户“秒杀结果正在计算中”,释放了系统资源之后再处理其它用户的请求。
我们会在后台启动若干个队列处理程序消费消息队列中的消息,再执行校验库存、下单等逻辑。因为只有有限个队列处理线程在执行,所以落入后端数据库上的并发请求是有限的。而请求是可以在消息队列中被短暂地堆积,当库存被消耗完之后,消息队列中堆积的请求就可以被丢弃了。
这就是消息队列在秒杀系统中最主要的作用:削峰填谷,也就是说它可以削平短暂的流量高峰,虽说堆积会造成请求被短暂延迟处理,但是只要我们时刻监控消息队列中的堆积长度,在堆积量超过一定量时,增加队列处理机数量来提升消息的处理能力就好了,而且秒杀的用户对于短暂延迟知晓秒杀的结果也是有一定容忍度的。
这里需要注意一下,我所说的是“短暂”延迟,如果长时间没有给用户公示秒杀结果,那么用户可能就会怀疑你的秒杀活动有猫腻了。所以在使用消息队列应对流量峰值时,需要对队列处理的时间、前端写入流量的大小、数据库处理能力做好评估,然后根据不同的量级来决定部署多少台队列处理程序。
比如你的秒杀商品有 1000 件,处理一次购买请求的时间是 500ms,那么总共就需要 500s 的时间。这时你部署 10 个队列处理程序,那么秒杀请求的处理时间就是 50s,也就是说用户需要等待 50s 才可以看到秒杀的结果,这是可以接受的。这时会并发 10 个请求到达数据库,并不会对数据库造成很大的压力。

通过异步处理简化秒杀请求中的业务流程

其实在大量的写请求“攻击”你的电商系统的时候,消息队列除了发挥主要的削峰填谷的作用之外,还可以实现异步处理来简化秒杀请求中的业务流程,提升系统的性能。
你想,在刚才提到的秒杀场景下,我们在处理购买请求时需要 500ms。这时你分析了一下整个的购买流程,发现这里面会有主要的业务逻辑,也会有次要的业务逻辑:比如说,主要的流程是生成订单、扣减库存;次要的流程可能是我们在下单购买成功之后会给用户发放优惠券,会增加用户的积分。
假如发放优惠券的耗时是 50ms,增加用户积分的耗时也是 50ms,那么如果我们将发放优惠券、增加积分的操作放在另外一个队列处理机中执行,那么整个流程就缩短到了 400ms,性能提升了 20%,处理这 1000 件商品的时间就变成了 400s。如果我们还是希望能在 50s 之内看到秒杀结果的话,只需要部署 8 个队列程序就好了。
经过将一些业务流程异步处理之后,我们的秒杀系统部署结构也会有所改变:

解耦实现秒杀系统模块之间松耦合

除了异步处理和削峰填谷以外,消息队列在秒杀系统中起到的另一个作用是解耦合。
比如数据团队对你说,在秒杀活动之后想要统计活动的数据,借此来分析活动商品的受欢迎程度、购买者人群的特点以及用户对于秒杀互动的满意程度等等指标。而我们需要将大量的数据发送给数据团队,那么要怎么做呢?
一个思路是:使用 HTTP 或者 RPC 的方式来同步地调用,也就是数据团队这边提供一个接口,我们实时将秒杀的数据推送给它,但是这样调用会有两个问题:
整体系统的耦合性比较强,当数据团队的接口发生故障时,会影响到秒杀系统的可用性。
当数据系统需要新的字段,就要变更接口的参数,那么秒杀系统也要随着一起变更。
这时,我们可以考虑使用消息队列降低业务系统和数据系统的直接耦合度。
秒杀系统产生一条购买数据后,我们可以先把全部数据发送给消息队列,然后数据团队再订阅这个消息队列的话题,这样它们就可以接收到数据,然后再做过滤和处理了。
秒杀系统在这样解耦合之后,数据系统的故障就不会影响到秒杀系统了,同时当数据系统需要新的字段时,只需要解析消息队列中的消息,拿到需要的数据就好了。
异步处理、解耦合和削峰填谷是消息队列在秒杀系统设计中起到的主要作用,其中异步处理可以简化业务流程中的步骤,提升系统性能;削峰填谷可以削去到达秒杀系统的峰值流量,让业务逻辑的处理更加缓和;解耦合可以将秒杀系统和数据系统解耦开,这样两个系统的任何变更都不会影响到另一个系统,
如果你的系统想要提升写入性能实现系统的低耦合,想要抵挡高并发的写流量,那么你就可以考虑使用消息队列来完成。

课程小结

本节课,我结合自己的实际经验,主要带你了解了消息队列在高并发系统设计中起到的作用以及一些注意事项,你需要了解的重点如下:
削峰填谷是消息队列最主要的作用,但是会造成请求处理的延迟。
异步处理是提升系统性能的神器,但是你需要分清同步流程和异步流程的边界,同时消息存在着丢失的风险,我们需要考虑如何确保消息一定到达。
解耦合可以提升你的整体系统的鲁棒性。
当然,你要知道,在使用消息队列之后虽然可以解决现有的问题,但是系统的复杂度也会上升。比如上面提到的业务流程中,同步流程和异步流程的边界在哪里?消息是否会丢失,是否会重复?请求的延迟如何能够减少?消息接收的顺序是否会影响到业务流程的正常执行?如果消息处理流程失败了之后是否需要补发?这些问题都是我们需要考虑的。我会利用接下来的两节课针对最主要的两个问题来讲讲解决思路:一个是如何处理消息的丢失和重复,另一个是如何减少消息的延迟。
引入了消息队列的同时也会引入了新的问题,需要新的方案来解决,这就是系统设计的挑战,也是系统设计独有的魅力,而我们也会在这些挑战中不断提升技术能力和系统设计能力。

一课一思

在今天的课程中,我提到了消息队列在高并发系统设计中起到的作用。那么你在开发过程中会在什么样的场景下使用消息队列呢?欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 28

提建议

上一篇
加餐 | 数据的迁移应该如何做?
下一篇
18 | 消息投递:如何保证消息仅仅被消费一次?
 写留言

精选留言(44)

  • Victor
    2019-10-28
    秒杀场景,使用消息队列的话,怎么保证秒杀产品不超卖,这块计算逻辑是怎么处理的?

    作者回复: 其实方法无非有几种: 1. 使用锁的方式,比如分布式锁,也可以利用redis本身操作原子性的特点 2. 写入消息队列,在消息队列中做减库存的操作,做异步校验

    共 22 条评论
    44
  • longslee
    2019-10-28
    打卡。太棒了,终于来到了消息队列环节。据说,有一种处理秒杀方式,是随机让一部分用户直接失败23333.
    共 10 条评论
    33
  • jun.hai
    2019-12-04
    老师您好,请教个问题:就是前面同学提到的在秒杀场景一下保证产品不超卖方案用redis的原子性特点,那么这个原子性在客户下单的时候用还是支付成功后才能用到?如果客户下单的时候用了redis的原子性减库存了,一旦客户不支付后取消订单了,这时候怎么处理已减掉的库存问题呢?谢谢

    作者回复: 我理解是在下单的时候,如果不支付,一般可以设置一个定时器,定时器时间一到,就把库存加上,同时定义订单失败

    共 2 条评论
    30
  • Kean
    2019-11-08
    比如1000件商品 系统生成1000个令牌 拿到令牌的用户可以进入消息队列,其他未拿到令牌的直接返回已抢完,这种方式是否可以?

    作者回复: 可以的

    共 10 条评论
    25
  • 啊啊啊哦哦
    2019-10-28
    用户如何知道结果轮循查询。还是长连接通知

    作者回复: 轮询查询耗费系统资源。简单的思路是电商系统一般会支持系统通知功能或者私信功能,可以给用户发一个私信:)

    共 9 条评论
    20
  • 撒旦的堕落
    2019-10-28
    负责过课程建设系统 由于课程对于一家教育网站是属于基础数据 所以课程的修改 比如增加了 一个章 需要通知课程审核系统 作业考试 视频学习 日志记录 在线大学等系统 用消息系统替换了rpc调用 降低了系统的响应时间 降低了系统之间的耦合 提升了系统的可维护性
    共 2 条评论
    13
  • Geek_Lee
    2019-10-29
    看过很多都是关于设计如何缓存,如何hash一致性分库分表等,,但是没有见过讲解服务容器等需求,,,希望老师能给我解答下,,比如说:10000QPS 需要多少台服务器,,需要多少tomcat类似等容器等 我的理解是 请求最终都会到达容器吧,容器扛不住,,上层设计的完美好像也不能完全解决??? 老师能帮我解答下吗??谢谢

    作者回复: 这个要看业务的复杂度,业务比较轻量的话,单台服务器抗2000qps没有问题,如果因为中有大量io请求,可能也就300-400qps,不能一概而论

    共 4 条评论
    11
  • 长期规划
    2019-12-21
    我理解这个解耦是为了提高整个系统的可用性。是指将业务系统的耦合转为业务系统与消息队列的耦合。即将多个系统的耦合转为对单一系统的耦合,而且这个单一系统没有业务逻辑,只存储。这两点可大大提高系统的可用性。不解耦的话,系统的可用性=A可用性 x B可用性 x C可用性... ,只要一个子系统不可用,整个系统就宕机了。解耦后只有所用子系统都不可用时,整个系统才不可用。

    作者回复: 可以这么理解~

    9
  • Bang
    2019-10-28
    秒杀场景使用了,消息队列,那么前端如何得获得秒杀结果呢? 消息队列的消费者,如何编写比较好, 是一个死循环监听程序吗?

    作者回复: 1. 可以使用产品的功能来通知,比如私信 2. 是的,是一个死循环的程序

    共 2 条评论
    6
  • Ricky Fung
    2019-12-23
    消息队列的应用场景:1.流量削峰(秒杀系统),2.异步处理(非核心流程异步处理 提升效率),3.系统解耦(用户下单后增加积分、通知仓储服务发货)。常用的消息中间件有rabbitmq、kafka、rocketmq。

    作者回复: 赞~

    共 4 条评论
    5
  • 阴建锋
    2020-03-29
    若系统要将所有请求(如:接口的请求与返回报文)的日志入库,如何实现,提高性能且不影响交易?

    作者回复: 一般用类似es的方案?

    共 2 条评论
    4
  • 程序水果宝
    2019-10-28
    消息队列的长度如何设计,以什么为参考?如果请求数量超过了所有消息队列的长度,怎么处理,直接丢弃就可以了吗?

    作者回复: 一般消息队列中间件支持堆积,也就是长度可以非常大。

    共 2 条评论
    4
  • 何磊
    2019-11-21
    请教两个问题: 1. 由于秒杀数量是有限的,那么假如商品只有1000,队列满1000后,其他请求是否应该直接拒流呢? 2. 用户请求入队列后,为了及时反馈结果,前端会不断轮训查询结果。查询的读求情也会消耗资源,这部分是采用缓存策略还是结合读写访问不同业务机器来处理呢?
    共 4 条评论
    3
  • GaGi
    2020-04-05
    对于这句话 “同步流程和异步流程的边界在哪里?”,老师可以列举一些例子吗?这个不是很清楚边界是指什么?

    作者回复: 就是哪些需要同步处理,哪些可以丢到队列处理机里面处理

    2
  • Eric
    2020-03-03
    秒杀请求数据库扛不住 转移到消息队列上的入队 那么消息队列能支撑很多并发的入队请求吗?

    作者回复: 看消息队列的性能了

    2
  • fomy
    2019-11-23
    为了系统之间的解耦,比如要对订单金额进行统计时。 异步处理,比如秒杀时,用Redis扣减库存,然后再异步下单,提高并发。 削峰填谷,对一些高并发的请求,先缓存到队列,然后再使用固定线程池消费,降低服务器压力。
    共 1 条评论
    2
  • 蓝魔丶
    2019-10-29
    数据同步、消息系统、短信系统、日志系统、事件广播等
    2
  • stg609
    2019-10-28
    老师,有没有哪节课会介绍下 在分布式高并发"写"的时候,可能遇到的问题及解决思路? 比如如何既保证数据的正确,又做到高性能? 比如分布式锁的利弊?actor模式的利弊等
    3
  • Bug? Feature!
    2019-10-28
    解耦,异步,削峰! 感谢老师的分享,谢谢!

    作者回复: :)

    2
  • 夜辉
    2021-04-05
    秒杀活动,让用户等50s后知道结果 没有即时性的反馈,用户体验不好吧,又不是众筹定时开奖活动
    共 1 条评论
    1