如何设计一个秒杀系统

1 前言

在电商行业,秒杀活动是常用的运营手段,为了实现商品促销、品牌推广、清理库存、拉新获客并增加用户粘性等目的,在秒杀活动中经常使用限时、限量、限定、低价甚至0元购等方式吸引用户抢购,例如京东618、淘宝双11等。由于秒杀活动价格低、数量少的特点,往往能吸引大量用户在短时间内进行秒杀操作。那么如何设计一个适用于秒杀活动的系统呢?接下来通过对业务场景、用户行为等进行分析,来看看设计一个秒杀系统需要做的事情都有哪些。

2 秒杀场景

这部分对秒杀活动中的场景进行分析,包括秒杀活动的实际业务场景、参加秒杀活动的用户行为、以及其它。

2.1 业务场景

1.大量用户在同一时间同时进行抢购。大量用户同时抢购,意味着在短时间内服务流量激增,根据秒杀活动力度不同,秒杀活动期间的服务流量可能比平时流量多几倍甚至几十倍。

2.访问请求数量远远大于库存数量。因为秒杀活动在伴随着低价的同时,同时会有限量的特点,因此往往大部分用户抢购失败,仅有极少部分用户抢购成功。

3.业务流程相对简单。由于秒杀活动一般都有门槛低、折扣大等优势,因此前期一般不需要其它复杂的营销逻辑,业务流程相对简单,基本上只是抢购成功减库存并通知用户的操作,后续订单、支付等业务流程与现有业务基本一致。

4.秒杀活动一般为临时活动。秒杀活动一般是在正常业务流程之外的临时活动,因此在部署和使用秒杀系统时不应该影响现有业务。

5.秒杀活动的目的是吸引目标用户。由于秒杀的目的大多数都是为了商品促销,增加用户粘性,吸引新用户等,因此对于一些恶意用户,机器人脚本等的秒杀行为应当加以限制。

6.秒杀活动甚至会低于成本价进行售卖。如果超卖,则意味着成本增加,因此应当严格限制秒杀活动中的库存数量。

2.2 用户行为

1.在临近秒杀时间点时,用户会频繁刷新页面。在秒杀倒计时,用户一般会频繁刷新页面,意味着在临近秒杀时的秒杀页面访问量会激增。

2.在秒杀开始时,用户会多次频繁点击秒杀按钮。这个时候秒杀接口访问量激增。

3.秒杀结束后用户急于查看秒杀结果。因此秒杀结果查询的接口访问量也会增加。

2.3 其它情况

1.单个用户使用多个设备或多个账号进行秒杀。对于电商平台来说,举办秒杀活动的目的是增加用户粘性,吸引新用户等,因此应当更多的将机会留给目标用户。

2.用户使用自动化软件进行作弊。同样的道理,应该对恶意的行为进行限制。

3 设计思路

3.1 动静分离部署,抵挡峰值流量

1.动静分离存储。将页面中的js,css,图片等静态存储在CDN中,访问秒杀页面时静态资源从CDN中获取,核心服务只提供秒杀的核心逻辑。对静态资源提前进行预热,由于CDN节点之间也会互相缓存,因此全国各地的用户请求页面时,都会从最近的CDN节点获取静态资源,用户访问页面的速度会大大提升。

2.缓存秒杀页面。秒杀页面进行动静分离后,但用户频繁刷新秒杀页面时,如果每次都请求后端,实际上对后端压力仍旧很大。Nginx除了可以实现负载均衡,还可以根据页面URL缓存页面,因此可以将秒杀页面缓存到Nginx中,即使后端服务宕机,只要Nginx没有挂掉,秒杀页面仍旧可以访问。OpenResty使得Nginx具备Web容齐功能,并可以修改Nginx中的静态页面数据,如果需要临时修改商品价格、库存数量、商品文案等信息,则可以使用OpenResty进行修改。

3.2 部分逻辑前置,简化秒杀逻辑

1.秒杀资格验证前置。可以在秒杀活动开始前几天就实行资格预约方案,由于预约时间段较长,一般不会出现高流量的情况,对即将参加秒杀的用户进行一些实名制、活跃度、是否新用户等验证,可以避免在秒杀时花费大量的算力进行验证,可以使用redis记录有验证通过有秒杀资格的用户,秒杀时只用简单验证一个用户存在redis中即有资格秒杀。

2.秒杀频率验证前置。秒杀开始时,用户多次点击秒杀按钮,而实际上有效的秒杀请求应该最多一次,因此可以在前端使用js控制用户点击秒杀按钮的频率,当用户点击完秒杀按钮后,即在前端将按钮置灰不可点击并显示倒计时,倒计时结束后按钮可再次点击。

3.秒杀结果查询前置。当用户秒杀成功时,后端首次将用户秒杀成功的结果返回给前端时,前端可以将秒杀成功的标志存入用户浏览器本地,当用户查询秒杀结果时,先从本地查询,如果已秒杀成功,则不需要往后端发送查询请求。

3.3 上游拦截请求,减轻下游压力

1.提前生成库存令牌。对于秒杀来说,一般情况下库存数量在秒杀结束之前就已经知晓,因此可以提前说生成库存相同数量的令牌放入redis的list中,当真正进行秒杀逻辑时,只用并发从list中pop令牌即可,pop出值则秒杀成功,否则失败。如果秒杀成功,则在秒杀成功的list中设置此用户的账号和库存令牌,并返回给前端秒杀成功,前端用户本地设置成功标记。

2.拦截秒杀的请求。虽然在前端使用js控制了用户点击秒杀按钮的频率,但是不能完全限制用户的点击,比如用户使用脚本、浏览器插件等自动将按钮改为可用,因此仍需要在后端进行请求频率验证。可以将用户的账号存入redis中加过期时间5秒,当用户再次请求秒杀到后端时,可以验证用户账号是否在redis中存在,如果存在则提示秒杀频率过快。

3.拦截查询秒杀结果的请求。当用户秒杀成功时,如果还未来得及在前端设置标记或者浏览器限制不允许写入,则查询请求仍旧会流入后端,因此可以在之前秒杀成功的list中查询结果,如果list中存在用户账号,则提示秒杀成功,前端尝试在用户本地设置标记。

3.4 异步处理请求,服务接口隔离

1.异步写库存。当秒杀成功时,除了返回给用户秒杀成功的通知,仍旧需要进行实际上的库存更新,虽然经过以上的请求拦截,真正的写库流量已经减少很多,但是由于时间较短,如果同步写库仍旧对数据库有较大压力。MQ有异步和削峰的特点,因此可以使用MQ进行异步库存更新和流量削峰,将库存更新消息发送到MQ中,下游库存服务从MQ中获取消息根据服务吞吐能力进行库存更新操作。而在库存更新中,考虑到服务宕机或消息重试等情况,应当考虑接口的幂等性。

2.异步发送通知。秒杀成功时,除了及时返回给前端秒杀结果,可能需要对用户发送邮件和短信进行通知,也可以使用MQ进行异步通知。

3.隔离秒杀接口和查询接口。部分用户秒杀成功后,可能去频繁查询秒杀结果,而部分用户可能还在秒杀中,因此需要隔离两个接口,互相不影响。

3.5 服务网关限流,中间件高可用

1.服务网关限流。可以在网关层检测异常流量,进行限流降级等,例如使用Google Guava的RateLimiter(令牌桶算法)进行大流量限流。

2.Nginx高可用。由于使用了Nginx进行页面缓存,部分请求压力前置到了Nginx,因此需要Nginx实现高可用,Keepalived可以监控集群中的服务节点状态,因此可以用来监控Nginx的运行状态,当Nginx集群中的某个节点宕机时,可以自动执行脚本进行重启以及主备切换。

3.Redis高可用。虽然在前面已经做了页面缓存、按钮置灰以及本地标记等工作,但是由于作弊脚本、自动化软件以及用户本地环境不同等情况的存在,实际上请求到后端的压力仍旧不小,而redis在查询秒杀资格、秒杀结果、库存令牌等方面实际上承受了后端大部分的压力,因此redis需要集群部署,可以使用Sentinel监听Redis集群进行主备切换,同时由于进程运行出错,网络拥塞等可能情况,Sentinel也需要进行集群部署以实现高可用。

4.MQ高可用。当流量走到MQ的时候,实际上流量已经不是很多,基本上跟库存数量有关, 但由于一般只是服务独立部署,中间件与其它日常业务共用,为避免临时的秒杀活动影响到日常业务(比如多个秒杀活动同时进行),MQ应当实现高可用,并避免消息丢失。

5.服务独立部署。微服务使用Docker快速部署上线下线,易于拓展。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!