秒杀技术架构的总结与思考

什么是秒杀?

在电商领域,存在着典型的秒杀业务场景,那何谓秒杀场景呢。简单的来说就是一件商品的购买人数远远大于这件商品的库存,而且这件商品在很短的时间内就会被抢购一空。 比如每年的618、双11大促,小米新品促销等业务场景,就是典型的秒杀业务场景。

秒杀有什么特点?

对于秒杀系统来说,我们可以从业务和技术两个角度来阐述其自身存在的一些特点。

业务特点:

(1)限时、限量、限价

(2)活动预热

(3)持续时间短

技术特点:

(1)瞬时并发量非常高

(2)读多写少

(3)流程简单

秒杀的技术挑战

1: 对现有业务冲击

秒杀活动具有时间短,并发访问量大的特点,如果和网站原有应用部署在一起,必然会对现有业务造成冲击,稍有不慎可能导致整个网站瘫痪。

解决思路:服务隔离,业务隔离,数据库隔离。秒杀服务单独部署,秒杀涉及的数据库也要单独部署,以防止影响其他业务

2:高并发的应用,对数据库压力大

秒杀主流程,查商品详情 -> 查库存 -> 请求扣减库存 -> 下单。 其中查商品详情页的在秒杀开始前的几分钟,用户会疯狂的刷新详情页,导致查询的请求量巨大,如果处理不当,有可能导致秒杀开始前,应用就瘫痪。

解决思路:上层应用缓存商品详情,最好的方法是静态化页面,定时推送到CDN上,商品详情页直接访问CDN。

3:商品超卖

秒杀开始时,同时会有很多用户请求秒杀接口,如果查询库存 -> 扣减库存 这两步操作如果不能保证原子操作。一定会导致超卖问题,造成资损。

解决思路:秒杀开始前,初始化库存到缓存中,在缓存中预减库存,下单失败再归还库存。

秒杀架构原则

1:尽量把请求拦截在上游,分散用户的并发时间。

第一层:客户端拦截

1: 输入验证码,图形验证码(12306)

2: 答题

3:客户端限制用户点击秒杀按钮的频率,强制用户等待。

第二层:后端拦截

1:通过在缓存中预减库存(秒杀库存远小于请求秒杀的用户数),能拦截掉大量无效请求。

2:防刷控制

3:使用MQ 消峰,把大量的并发请求转换为排队请求。

2:把数据放在离用户最近的地方

1:使用CDN静态化秒杀商品数据

2:优先使用本地缓存

秒杀后端架构设计

1:全异步方案:

image.png

1:用户发起秒杀请求

用户发起秒杀请求后,商城服务会经过如下业务流程。

(1)检测验证码是否正确

用户发起秒杀请求时,会将验证码一同发送过来,系统会检验验证码是否有效,并且是否正确。

2)是否限流

系统会对用户的请求进行是否限流的判断,这里,我们可以通过判断消息队列的长度来进行判断。因为我们将用户的请求放在了消息队列中,消息队列中堆积的是用户的请求,我们可以根据当前消息队列中存在的待处理的请求数量来判断是否需要对用户的请求进行限流处理。

例如,在秒杀活动中,我们出售1000件商品,此时在消息队列中存在1000个请求,如果后续仍然有用户发起秒杀请求,则后续的请求我们可以不再处理,直接向用户返回商品已售完的提示。

image

所以,使用限流后,我们可以更快的处理用户的请求和释放连接的资源。这个地方存在一些问题:如果多个秒杀商品共用一个消息队列主题,就无法使用判断队列长度的方式来限流。

3)发送MQ

用户的秒杀请求通过前面的验证后,我们就可以将用户的请求参数等信息发送到MQ中进行异步处理,同时,向用户响应结果信息。在商城服务中,会有专门的异步任务处理模块来消费消息队列中的请求,并处理后续的异步流程。

在用户发起秒杀请求时,异步下单流程比同步下单流程处理的业务操作更少,它将后续的操作通过MQ发送给异步处理模块进行处理,并迅速向用户返回响应结果,释放请求连接。

2:异步处理

我们可以将下单流程的如下操作进行异步处理。

(1)判断活动是否已经结束

(2)判断本次请求是否处于系统黑名单,为了防止电商领域同行的恶意竞争可以为系统增加黑名单机制,将恶意的请求放入系统的黑名单中。可以使用拦截器统计访问频次来实现。

(3)扣减缓存中的秒杀商品的库存数量。

(4)生成秒杀Token,这个Token是绑定当前用户和当前秒杀活动的,只有生成了秒杀Token的请求才有资格进行秒杀活动。

这里我们引入了异步处理机制,在异步处理中,系统使用多少资源,分配多少线程来处理相应的任务,是可以进行控制的。

3:轮询结果

这里,可以采取客户端短轮询查询是否获得秒杀资格的方案。例如,客户端可以每隔3秒钟轮询请求服务器,查询是否获得秒杀资格,这里,我们在服务器的处理就是判断当前用户是否存在秒杀Token,如果服务器为当前用户生成了秒杀Token,则当前用户存在秒杀资格。否则继续轮询查询,直到超时或者服务器返回商品已售完或者无秒杀资格等信息为止。

 

采用短轮询查询秒杀结果时,在页面上我们同样可以提示用户排队处理中,但是此时客户端会每隔几秒轮询服务器查询秒杀资格的状态,相比于同步下单流程来说,无需长时间占用请求连接。

 

此时,可能会有网友会问:采用短轮询查询的方式,会不会存在直到超时也查询不到是否具有秒杀资格的状态呢?答案是:有可能! 这里我们试想一下秒杀的真实场景,商家参加秒杀活动本质上不是为了赚钱,而是提升商品的销量和商家的知名度,吸引更多的用户来买自己的商品。所以,我们不必保证用户能够100%的查询到是否具有秒杀资格的状态。

4:秒杀结算

(1)验证下单Token

客户端提交秒杀结算时,会将秒杀Token一同提交到服务器,商城服务会验证当前的秒杀Token是否有效。

(2)加入秒杀购物车

商城服务在验证秒杀Token合法并有效后,会将用户秒杀的商品添加到秒杀购物车。

5:提交订单

(1)订单入库

将用户提交的订单信息保存到数据库中。

(2)删除Token

秒杀商品订单入库成功后,删除秒杀Token。

这里大家可以思考一个问题:我们为什么只在异步下单流程的粉色部分采用异步处理,而没有在其他部分采取异步削峰和填谷的措施呢?

 

这是因为在异步下单流程的设计中,无论是在产品设计上还是在接口设计上,我们在用户发起秒杀请求阶段对用户的请求进行了限流操作,可以说,系统的限流操作是非常前置的。在用户发起秒杀请求时进行了限流,系统的高峰流量已经被平滑解决了,再往后走,其实系统的并发量和系统流量并不是非常高了。

 

所以,网上很多的文章和帖子中在介绍秒杀系统时,说是在下单时使用异步削峰来进行一些限流操作,那都是在扯淡! 因为下单操作在整个秒杀系统的流程中属于比较靠后的操作了,限流操作一定要前置处理,在秒杀业务后面的流程中做限流操作是没啥卵用的。

 

异步方案缺点:

1:token的保存,所有的用户都需要轮询保存token的缓存,如果库存过大,本地缓存可能存不下甚至还需要做本地缓存同步,如果都查询远程缓存(redis),也会造成热点key问题。

2:没有秒杀成功的用户需要等待轮询超时

 

2:同步方案

image.png

同步方案和异步方案的区别点就是不再使用MQ来做限流降级,而是使用本地缓存实现库存扣减,本地缓存扣减成功才扣减远程缓存(redis) 只有两个缓存都扣减成功才算真正扣减库存成功,才给用户返回秒杀成功的token。

具体的扣库存原理如下(拿买火车票举例):

本地扣库存。我们把一定的库存量分配到本地机器,直接在内存中减库存,然后按照之前的逻辑异步创建订单。改进过之后的单机系统是这样的:

 

image

 

这样就避免了对数据库频繁的IO操作,只在内存中做运算,极大的提高了单机抗并发的能力。但是百万的用户请求量单机是无论如何也抗不住的,虽然nginx处理网络请求使用epoll模型,c10k的问题在业界早已得到了解决。但是linux系统下,一切资源皆文件,网络请求也是这样,大量的文件描述符会使操作系统瞬间失去响应。我们可以把秒杀服务集群化,比如增加100台服务器,这样单机所承受的并发量就小了很多。然后我们每台机器本地库存100张火车票,100台服务器上的总库存还是1万,这样保证了库存订单不超卖,下面是我们描述的集群架构:

image.png

问题接踵而至,在高并发情况下,现在我们还无法保证系统的高可用,假如这100台服务器上有两三台机器因为扛不住并发的流量或者其他的原因宕机了。那么这些服务器上的订单就卖不出去了,这就造成了订单的少卖。要解决这个问题,我们需要对总订单量做统一的管理,这就是接下来的容错方案。服务器不仅要在本地减库存,另外要远程统一减库存。有了远程统一减库存的操作,我们就可以根据机器负载情况,为每台机器分配一些多余的“buffer库存”用来防止机器中有机器宕机的情况。我们结合下面架构图具体分析一下:


image

 

我们采用Redis存储统一库存,因为Redis的性能非常高,号称单机QPS能抗10W的并发。在本地减库存以后,如果本地有订单,我们再去请求redis远程减库存,本地减库存和远程减库存都成功了,才返回给用户抢票成功的提示,这样也能有效的保证订单不会超卖。当机器中有机器宕机时,因为每个机器上有预留的buffer余票,所以宕机机器上的余票依然能够在其他机器上得到弥补,保证了不少卖。buffer余票设置多少合适呢,理论上buffer设置的越多,系统容忍宕机的机器数量就越多,但是buffer设置的太大也会对redis造成一定的影响。虽然redis内存数据库抗并发能力非常高,请求依然会走一次网络IO,其实抢票过程中对redis的请求次数是本地库存和buffer库存的总量,因为当本地库存不足时,系统直接返回用户“已售罄”的信息提示,就不会再走统一扣库存的逻辑,这在一定程度上也避免了巨大的网络请求量把redis压跨,所以buffer值设置多少,需要架构师对系统的负载能力做认真的考量。

 

扣库存的优化方案:

无论是同步方案还是异步方案,都存在库存过大时,远程缓存热点key的问题。

如何解决热点key的问题,利用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)。

image

此时,我们将库存进行分割后,每个分割后的库存使用商品id加上一个数字标识来存储,这样,在对存储商品库存的每个Key进行Hash运算时,得出的Hash结果是不同的,这就说明,存储商品库存的Key有很大概率不在Redis的同一个槽位中,这就能够提升Redis处理请求的性能和并发量。

分割库存后,我们还需要在Redis中存储一份商品id和分割库存后的Key的映射关系,此时映射关系的Key为商品的id,也就是10001,Value为分割库存后存储库存信息的Key,也就是10001_0,10001_1,10001_2,10001_3,10001_4。在Redis中我们可以使用List来存储这些值。

 

优酷体育能量商城的秒杀技术方案

优酷体育的方案采用了全异步 + 库存分桶方案,具体方案如下:

缓存模块

商品详情1s本地缓存。 预计 数据库 QPS 200 5ms

商城秒杀列表1s本地缓存 (倒计时不缓存)。 数据库 QPS 100 10ms

商城商品列表本地缓存5秒。

image.png

限流模块:

image.png

库存控制模块:

redis exincrBy  支持最大值限制:  每个桶最大能发放10000 , 回滚库存时: 增加能发放的最大值。

支持秒杀过程中,运营新增库存,支持本地桶上线下线控制

image.png

桶的上线下线逻辑:

image.png

 

整体链路流程

秒杀初始化链路流程:

image.png

秒杀流程:

image.png

 

 

posted @ 2022-01-07 15:26  泉水姐姐。  阅读(757)  评论(0编辑  收藏  举报