🚀【高并发技术专题】你需要了解的秒杀方案
一、前言
很多小伙伴反馈说,高并发学了那么久,但是在真正做项目时,仍然不知道如何下手处理高并发业务场景!甚至很多小伙伴仍然停留在只是简单的提供接口(CRUD)阶段,不知道学习的并发知识如何运用到实际项目中,就更别提如何构建高并发系统了!
究竟什么样的系统算是高并发系统?今天,我们就一起解密高并发业务场景下典型的秒杀系统的架构,让大家学以致用。
二、电商系统架构
在电商领域,存在着典型的秒杀业务场景,那何谓秒杀场景呢。简单的来说就是一件商品的购买人数远远大于这件商品的库存,而且这件商品在很短的时间内就会被抢购一空。比如每年的618、双11大促,小米新品促销等业务场景,就是典型的秒杀业务场景。
我们可以将电商系统的架构简化成下图所示:

由图所示,我们可以简单的将电商系统的核心层分为:负载均衡层、应用层和持久层。接下来,我们就预估下每一层的并发量。
假如负载均衡层使用的是高性能的Nginx,则我们可以预估Nginx最大的并发度为:10W+,这里是以万为单位。 假设应用层我们使用的是Tomcat,而Tomcat的最大并发度可以预估为800左右,这里是以百为单位。 假设持久层的缓存使用的是Redis,数据库使用的是MySQL,MySQL的最大并发度可以预估为1000左右,以千为单位。Redis的最大并发度可以预估为5W左右,以万为单位。
所以,负载均衡层、应用层和持久层各自的并发度是不同的,那么,为了提升系统的总体并发度和缓存,我们通常可以采取哪些方案呢?
1. 系统扩容
2. 缓存
3. 读写分离
三、秒杀系统的特点
对于秒杀系统来说,我们可以从业务和技术两个角度来阐述其自身存在的一些特点。
1. 业务特点
这里,我们可以使用12306网站来举例,每年春运时,12306网站的访问量是非常大的,但是网站平时的访问量却是比较平缓的,也就是说,每年春运时节,12306网站的访问量会出现瞬时突增的现象。
再比如,小米秒杀系统,在上午10点开售商品,10点前的访问量比较平缓,10点时同样会出现并发量瞬时突增的现象。
所以,秒杀系统的流量和并发量我们可以使用下图来表示:

由图可以看出,秒杀系统的并发量存在瞬时凸峰的特点,也叫做流量突刺现象。
我们可以将秒杀系统的特点总结如下:

①. 限时、限量、限价
②. 活动预热
需要提前配置活动;活动还未开始时,用户可以查看活动的相关信息;秒杀活动开始前,对活动进行大力宣传。
③. 持续时间短
购买的人数数量庞大;商品会迅速售完。
在系统流量呈现上,就会出现一个突刺现象,此时的并发访问量是非常高的,大部分秒杀场景下,商品会在极短的时间内售完。
2. 技术特点
我们可以将秒杀系统的技术特点总结如下:

①. 瞬时并发量非常高
②. 读多写少
③. 流程简单
秒杀系统的业务流程一般比较简单;总体上来说,秒杀系统的业务流程可以概括为:下单减库存。
针对这种短时间内大流量的系统来说,就不太适合使用系统扩容了,因为即使系统扩容了,也就是在很短的时间内会使用到扩容后的系统,大部分时间内,系统无需扩容即可正常访问。那么,我们可以采取哪些方案来提升系统的秒杀性能呢?
四、秒杀系统方案
针对秒杀系统的特点,我们可以采取如下的措施来提升系统的性能。

①. 异步解耦
②. 限流防刷
③. 资源控制
由于应用层能够承载的并发量比缓存的并发量少很多。
如果在秒杀活动开始时,并发量太高时,我们可以将用户的请求放入队列中进行处理,并为用户弹出排队页面。
五、秒杀系统时序图
网上很多的秒杀系统和对秒杀系统的解决方案,并不是真正的秒杀系统,他们采用的只是同步处理请求的方案,一旦并发量真的上来了,他们所谓的秒杀系统的性能会急剧下降。
1. 下单流程
我们先来看一下秒杀系统在同步下单时的时序图:

①. 用户发起秒杀请求
识别验证码是否正确
判断活动是否已经结束
验证访问请求是否处于黑名单
验证真实库存是否足够
扣减缓存中的库存
计算秒杀的价格
②. 提交订单
订单入口
扣减真实库存
如果我们使用上述流程开发了一个秒杀系统,当用户发起秒杀请求时,由于系统每个业务流程都是串行执行的,整体上系统的性能不会太高,当并发量太高时,我们会为用户弹出下面的排队页面,来提示用户进行等待。
如果12306、淘宝、天猫、京东、小米等大型商城的秒杀系统是这么玩的话,那么,他们的系统迟早会被玩死,他们的系统工程师不被开除才怪!所以,在秒杀系统中,这种同步处理下单的业务流程的方案是不可取的。
以上就是同步下单的整个流程操作,如果下单流程更加复杂的话,就会涉及到更多的业务操作。
2. 异步下单流程
既然同步下单流程的秒杀系统称不上真正的秒杀系统,那我们就需要采用异步的下单流程了。异步的下单流程不会限制系统的高并发流量。

①. 用户发起秒杀请求
用户发起秒杀请求后,商城服务会经过如下业务流程。
检测验证码是否正确
用户发起秒杀请求时,会将验证码一同发送过来,系统会检验验证码是否有效,并且是否正确。
是否限流
系统会对用户的请求进行是否限流的判断,这里,我们可以通过判断消息队列的长度来进行判断。因为我们将用户的请求放在了消息队列中,消息队列中堆积的是用户的请求,我们可以根据当前消息队列中存在的待处理的请求数量来判断是否需要对用户的请求进行限流处理。
例如,在秒杀活动中,我们出售1000件商品,此时在消息队列中存在1000个请求,如果后续仍然有用户发起秒杀请求,则后续的请求我们可以不再处理,直接向用户返回商品已售完的提示。
所以,使用限流后,我们可以更快的处理用户的请求和释放连接的资源。
发送MQ
用户的秒杀请求通过前面的验证后,我们就可以将用户的请求参数等信息发送到MQ中进行异步处理,同时,向用户响应结果信息。在商城服务中,会有专门的异步任务处理模块来消费消息队列中的请求,并处理后续的异步流程。
用户发起秒杀请求时,异步下单流程比同步下单流程处理的业务操作更少,它将后续的操作通过MQ发送给异步处理模块进行处理,并迅速向用户返回响应结果,释放请求连接。
②. 异步处理
我们可以将下单流程的如下操作进行异步处理。
判断活动是否已经结束。 判断本次请求是否处于系统黑名单,为了防止电商领域同行的恶意竞争可以为系统增加黑名单机制,将恶意的请求放入系统的黑名单中。可以使用拦截器统计访问频次来实现。 扣减缓存中的秒杀商品的库存数量。 生成秒杀Token,这个Token是绑定当前用户和当前秒杀活动的,只有生成了秒杀Token的请求才有资格进行秒杀活动。
这里我们引入了异步处理机制,在异步处理中,系统使用多少资源,分配多少线程来处理相应的任务,是可以进行控制的。
③. 短轮询查询秒杀结果
采用短轮询查询秒杀结果时,在页面上我们同样可以提示用户排队处理中,但是此时客户端会每隔几秒轮询服务器查询秒杀资格的状态,相比于同步下单流程来说,无需长时间占用请求连接。
此时,可能会有网友会问:采用短轮询查询的方式,会不会存在直到超时也查询不到是否具有秒杀资格的状态呢?
④. 秒杀结算
验证下单Token
加入秒杀购物车
⑤. 提交订单
订单入库
删除Token
六、高并发“黑科技”与致胜奇招
假设,在秒杀系统中我们使用Redis实现缓存,假设Redis的读写并发量在5万左右。我们的商城秒杀业务需要支持的并发量在100万左右。如果这100万的并发全部打入Redis中,Redis很可能就会挂掉,那么,我们如何解决这个问题呢?接下来,我们就一起来探讨这个问题。
在高并发的秒杀系统中,如果采用Redis缓存数据,则Redis缓存的并发处理能力是关键,因为很多的前缀操作都需要访问Redis。而异步削峰只是基本的操作,关键还是要保证Redis的并发处理能力。
解决这个问题的关键思想就是:分而治之,将商品库存分开放。
1. 暗度陈仓
我们在Redis中存储秒杀商品的库存数量时,可以将秒杀商品的库存进行“分割”存储来提升Redis的读写并发量。
例如,原来的秒杀商品的id为10001,库存为1000件,在Redis中的存储为(10001, 1000),我们将原有的库存分割为5份,则每份的库存为200件,此时,我们在Redia中存储的信息为(10001_0, 200),(10001_1, 200),(10001_2, 200),(10001_3, 200),(10001_4, 200)。

此时,我们将库存进行分割后,每个分割后的库存使用商品id加上一个数字标识来存储,这样,在对存储商品库存的每个Key进行Hash运算时,得出的Hash结果是不同的,这就说明,存储商品库存的Key有很大概率不在Redis的同一个槽位中,这就能够提升Redis处理请求的性能和并发量。
在真正处理库存信息时,我们可以先从Redis中查询出秒杀商品对应的分割库存后的所有Key,同时使用AtomicLong来记录当前的请求数量,使用请求数量对从Redia中查询出的秒杀商品对应的分割库存后的所有Key的长度进行求模运算,得出的结果为0,1,2,3,4。再在前面拼接上商品id就可以得出真正的库存缓存的Key。此时,就可以根据这个Key直接到Redis中获取相应的库存信息。
2. 移花接木
在高并发业务场景中,我们可以直接使用Lua脚本库(OpenResty)从负载均衡层直接访问缓存。
为了解决这个问题,此时,我们可以在系统的负载均衡层取出用户发送请求时携带的用户id,商品id和秒杀活动id等信息,直接通过Lua脚本等技术来访问缓存中的库存信息。如果秒杀商品的库存小于或者等于0,则直接返回用户商品已售完的提示信息,而不用再经过应用层的层层校验了。针对这个架构,我们可以参见本文中的电商系统的架构图(正文开始的第一张图)。