You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

101 lines
12 KiB
Markdown

2 years ago
# 17 | 高性能架构案例:如何设计一个秒杀系统?
你好,我是王庆友。
在上一讲中我和你详细介绍了打造一个高性能系统的应对策略和架构手段。那么今天我就以1号店的秒杀系统为例来具体说明如何实现一个高性能的系统。
## 背景和问题
先说下背景。在2014年的时候1号店作为网上超市类电商经常在线上举行各种大促活动。比如进口牛奶促销活动每次促销的牛奶有几十万盒促销价格非常优惠一般这样的促销活动会在某个整点的时间进行开卖如上午10点。对于这种质高价优并且是刚需的商品会有大量的用户来抢购俗话说“手快有手慢无”往往短短几分钟内所有牛奶就能售卖完毕。
这本质上是一种秒杀活动,但商品数量非常大,一瞬间会有大量的用户流量涌入,流量可以高达平时的几十倍。而且和少量商品的秒杀不同,这些都是有效流量,最终会生成订单。
而在正常情况下系统因为资源有限只能处理10%的流量无法处理剩下的90%流量,瞬间高并发的流量涌入,很大程度上会引起后台系统超时报错,导致用户下单不成功。这样一来,用户就会反复刷新页面,多次尝试下单,不但用户的体验不好,而且系统的压力会更大。
**最终的结果就是,系统往往由于过载,整体处理能力下降,甚至瘫痪,导致所有用户都无法购买。**就像下图表示的一样,在秒杀场景下,系统会面临这样的困境:
![](https://static001.geekbang.org/resource/image/e9/31/e933d73f5e41c6ef89080cf56be90031.jpg)
在这种情况下,对于用户来说,能不能买到商品,拼的是体力和人品,由于体验不好,用户会逐渐对活动失去兴趣;而对于系统来说,我们需要拼命地加机器来满足峰值流量。
每次1号店要进行大促的时候在活动开始前运营和技术人员会坐在一起大家一起来预估活动的峰值流量然后技术人员做评估系统的哪些节点需要加机器以及要加多少机器。但这样的做法其实存在几个问题
* 首先,我们对峰值流量的预估以及要加多少机器都是拍脑袋的,和实际出入往往很大,一旦估计少了,系统同样会面临过载的风险;
* 其次,为了短暂的几分钟促销,我们需要增加大量的机器,事先要做很多的运维准备工作,不但浪费资源,而且效率很低;
* 最为关键的是,有些处理节点,系统不是通过加机器就能扩展处理能力的,比如商品库存数据库,下单时,我们需要扣库存,而为了防止库存更新冲突,我们需要锁定库存记录,导致系统的并发处理能力有限,这个问题单靠加机器是解决不了的。
## 总体方案
对于这种高并发情况,看来让系统单纯地通过加机器去硬扛,是不可行的。**那么,我们有没有更好的办法,既保证用户体验,又保证系统能够轻松地应对流量挑战呢?**
我们先来深入分析下业务场景。这个秒杀活动的特点是在短期的1~2分钟内用户流量很大但只要促销的商品卖完流量马上恢复常态。所以对于前端短期内这么大的下单请求后端如果实时处理压力会非常大但如果把这个处理时间延长到10分钟后端是可以完成下单的。那对用户来说商品优惠的力度这么大他们关心的是能否买到所以会愿意多等一段时间而不是在页面上一次次点击下单每次系统都提示下单失败。
当然,如果我们把订单处理的时间延长了,只要我们在前台告诉用户,系统已经接受了他们的订单,并且不断同步用户订单处理的进度,用户体验的问题其实也不大。
基于这个分析,我们就可以利用**异步处理**的思路来应对秒杀活动。
我们先在前端接收用户所有的下单请求但不在后端实时生成订单而是放在队列里然后系统根据后端订单中心的实际处理能力从队列里获取订单请求再交给订单中心生成实际的订单。同时系统告诉用户当前的处理进度有多少订单排在TA的前面TA还要等多久。
这样对于用户来说,在前台下单一次就可以了,然后等系统慢慢处理,这也符合先到先得的原则,非常公平合理。对系统来说,只要根据大促的商品总量,一定程度上增强系统处理能力,保证下单请求从进来到最后处理完成,这个时间相对合理就可以了。
比如说有20万件的商品每人限购一件预计用户会在2分钟内完成下单但用户能够接受系统在20分钟内完成订单处理。这样系统只要保证每分钟能处理1万订单就行而如果不采取排队的方式系统就需要每分钟处理10万订单它的压力就会提升一个数量级。
基于排队的思路,系统总体架构设计如下图所示:
![](https://static001.geekbang.org/resource/image/6a/7f/6a809c9da997868271c2b493cb4f397f.jpg)
在这个架构中,我们**在前台和后台下单系统之间,新增了排队系统,它包括排队区和处理区两个部分。**系统整体的处理过程是这样的:
1. 用户在商品详情页提交订单后,这个订单会作为预订单进入排队区,同时排队系统会返回用户一个排队编号,这个编号用于跟踪后续的订单处理进度;
2. 用户被引导到一个等待页,这个页面会根据排队号,定时地查询排队系统,排队系统会返回预订单在队列中的位置信息,包括它前面还有多少未处理的预订单,以及后台系统大概还要多久会处理这个预订单,这样用户就不会焦虑;
3. 在排队系统的处理区,有很多消费者,它们依次从排队区的队列里获取预订单,然后调用后台下单系统生成实际的订单;
4. 随着预订单变成正式的订单,队列里的预订单会逐渐变少,如果当前的预订单已经从队列里被移除了,用户的等待页就会检测到这个情况,页面自动跳转到订单完成页,这就和常规的购物流程一样了,用户进行最后的支付,最终完成整个前台下单过程。
这里,你可以看到,**前台**的预订单有瞬时的大流量,但我们只是把它们放到队列里,这个处理起来很快,排队系统可以轻松应对;而**后台**生成实际的订单是匀速的,并且最大化地发挥了下单系统的处理能力。另一方面,对于用户体验来说,用户可以选择在等待页等候,实时获取订单处理进度的反馈,也可以选择离开,然后在用户中心的“待支付订单”里完成支付。通过这样的设计,排队系统既保证了系统处理订单的能力,也保证了用户良好的体验。
下面是一张用户等待页的效果图,你可以直观地了解秒杀系统的用户体验。
![](https://static001.geekbang.org/resource/image/c8/30/c8e4840b1e3064ecf84b6c77d38b9630.jpg)
现在,你已经了解了秒杀系统的总体设计。接下来,我深入介绍下这个排队系统的内部设计细节,帮助你更好地理解它。
## 内部设计
首先,**针对队列的技术选型**,排队系统使用的是**Redis**而不是MQ。因为相对于MQ来说Redis更轻量级性能更好它内置了队列数据结构除了和MQ一样支持消息的先进先出以外我们还可以获取队列的长度以及通过排队号获取消息在队列中的位置这样我们就可以给前端反馈预订单的处理进度。
对于秒杀场景来说,一个订单只能包含一个商品,这里我们**为每个秒杀商品提供一个单独的队列**这样就可以分散数据在Redis中的存取多个队列可以提供更好的性能。
**关于队列的调度问题**,也就是消费者优先从哪个队列里拿预订单,排队系统会结合下单时间和队列的长度来确定,以保证用户合理的时间体验。比如说,某个秒杀商品的队列很长,消费者会优先从这个队列拿预订单,从而避免用户等待太长的时间。
**关于队列长度**为了保证用户能够买到商品我们并不是把所有前台的下单请求都会放到队列里而是根据参与活动的秒杀商品数量按照1:1的比例设置队列初始长度这样就保证了进入队列的请求最终都能生成订单。
这个可用队列长度会随着预订单进入队列不断地减少当数值变为0时下单前台会拒绝接受新请求进入队列直接反馈用户下单失败。当然如果后台订单生成异常或用户取消订单后可用队列长度会增加前台会重新开放预订单进入队列。
## 更多优化:建立活动库存
除了秒杀流程,系统还有**常规的购物流程**,这两个购物方式都是从详情页开始,到订单完成页结束。不同的地方是,常规购物流程走的是购物车和结算页,系统是同步处理的,这样可以有更好的用户体验。
在这里,我们在系统设计上,可以很好地同时支持秒杀流程和常规购物流程。
如果运营人员在后台上架商品的时候,设置这是一个秒杀商品,那么从详情页开始,系统就会引导用户走秒杀流程,否则就走常规购物流程。特别是在早期秒杀系统刚落地的时候,如果发现秒杀流程有问题,我们还可以快速切回到常规的购物流程,实现了一定程度上的系统互备。
![](https://static001.geekbang.org/resource/image/74/75/74d1a5c896d5f21f42c202f4f3d3bc75.jpg)
此外,对于秒杀活动来说,参与活动的商品种类是有限的,但这些商品库存的扣减非常频繁,因此我们建立了**活动库存**的概念,把少量参与促销的商品种类单独放在一个库里,避免和大量常规的商品放在一起,这样也大幅度地提高了库存数据库的读写性能。
好了,通过这个秒杀系统的架构设计,你可以看到,我们巧妙地通过请求的异步化处理,对流量进行削峰,从而保证了系统的高性能。这里我们不需要增加太多的机器,在系统落地时,我们通过排队系统对前后台解耦,后台下单系统基本上也不需要修改,系统整体改造的工作量不大,整个落地过程也非常顺利。
不过值得注意的是,**这种方式比较适合瞬时有高并发流量的场景**,比如这里说的秒杀场景。如果订单高峰会持续一段较长的时间,而用户对订单处理又有比较高的时间要求,那就不适合采用这种异步削峰的方式。
举个例子,外卖订单的午高峰通常会持续两个小时,而用户普遍期待订单半小时能够送达。对于这种情况,我们就需要正面应对高峰流量,比如通过水平扩展各个节点,提升系统的处理能力。这也要求系统能够做到弹性伸缩,高效地支持资源的缩容或扩容,节省成本。
## 总结
今天我针对1号店的大促业务挑战与你分享了一个秒杀系统的具体设计对照我在上一讲中介绍的高性能应对策略秒杀系统主要使用了**异步化处理**的方式,这也符合实际的业务场景。
通过今天的分享,相信你对如何保障系统的高性能有了更深入的体会,如果你也有类似的瞬时高并发的场景,你也可以在实践中参考这里的做法。
**最后,给你留一道思考题:** 你的公司业务上有高并发的场景吗,系统是如何应对的呢?
欢迎给我留言,我会及时给你反馈。如果这节课对你有帮助,也欢迎你把它分享给你的朋友。感谢你的阅读,我们下期再见。