设计一个秒杀系统
如何设计一个秒杀系统?
秒杀系统需要考虑的问题?
瞬时并发量大
- 大量用户会在同一时间进行抢购
- 网站瞬时访问流量激增
库存少
- 访问请求数量远远大于库存数量
- 只有少部分用户能够秒杀成功
前端
CDN
一般情况下,秒杀的流量特性就是持续性短和大
流量集中在活动即将开始的时候,会有很多用户开始持续性地刷新页面。前端资源的访问也需要损耗大量的资源因此需要利用 CDN 缓存秒杀页面的一些静态资源(如商品详情,图片等),将这部分压力给到CDN 厂商。并且静态资源放在 CDN 厂商那之后,地理位置也距离用户更近,用户访问也就更快,体验上也更好。

按钮防抖
点击后禁用按钮,防止用户重复多次点击发出大量请求,同时可以设置验证码防止非法用户。
访问层
负载均衡
如果前端请求发出来了,那么可以利用 Nginx 统一接入,针对更大的流量可以在 Nginx 前面再加 LVS
LVS四层转发请求打到多台 Nginx 上, Nginx 再负载均衡到多台后端服务,且 Nginx 有限流功能,例如 ip 限流,还可以配置黑名单等等,其实已经可以拦截大量请求流量
Nginx负载均衡到网关后,会通过客户端的负载均衡器,如Ribbon来进行服务的分发,这里的4级负载均衡基本就可以处理每秒10w以上的QPS并发

动态伸缩
通常这种秒杀服务都会结合Docker/K8S进行云服务器的动态伸缩部署,因为这种秒杀并不是一直用到的,所以当秒杀出现时我们就可以动态扩容服务器节点数量,当秒杀结束时自动缩减服务器节点数量,这样可以有效的利用服务器的资源。
限流
秒杀场景中可能参杂很多羊毛党,黑产等无效的流量,风控识别黑产,进行流量防控且需要动态黑名单机制
可以在Nginx层需要配置好限流,黑名单等 ,同时在网关层,通过比如sentinel对不同的服务节点设置限流或者熔断
服务层
缓存
在秒杀开始之前,可以将秒杀需要的商品,库存等信息提前预热到Redis当中,防止数据库被击穿
然后在秒杀开始的时候利用 redis +lua 脚本控制库存的扣减。
lua 脚本的内容分为三步:
- 根据商品 key 获取库存
- 如果有则库存-1,返回新库存
- 如果没库存,则返回没库存
redis + lua 可以保证操作的原子性,且性能足够优秀,因此是一个非常高效的库存扣减方案。
消息队列
通过MQ进行削峰填谷,可以减轻下游的压力,防止激增流量打垮下游的数据库
Redis 扣减完毕之后,可以发送一个异步消息(消息队列削峰填谷),后端服务异步消费把数据库中的库存给扣了,实现最终一致性。

Redis 操作成功后,mq发送失败怎么办?
因此,我们还需要一个准实时对账机制,lua 脚本内不仅要扣减库存,还需要利用 zset 增加流水,score 设置为时间。定时拉取一段时间流水记录比对数据库的库存是否一致,如果不一致则补偿。
本地缓存
至于本地缓存,理论上性能更高,但是方案设计上会更复杂,因为库存被分配到多个应用中。需要在秒杀预热的时候,给后端服务预分配好库存,然后应用各自承接库存扣减,也需要做好对账,防止意外的发生
库存扣减
数据库使用乐观锁防止库存超卖
1 |
|
如果使用这个语句,在高并发场景下,实际上就会产生热点行问题。
数据库热点行问题解决方案
使用数据库补丁优化
如果数据库用的是阿里云的 RDS,实际上有一个可落地的优化方案:Inventory hint + Returning。如果你公司本身用的就是阿里云的 RDS,这个改造成本就很低,仅需在 SQL 上填写一些 hint 即可。在SQL表名前加/+COMMIT ON SUCCESS ROLLBACK ON_FAIL TARGET AFFECT ROW(1)/
Inventory hint
- COMMIT ON SUCCESS:当前语句执行成功就提交事务上下文。
- ROLLBACK ON FAIL:当前语句执行失败就回滚事务上下文。
- TARGET AFFECT ROW(NUMBER):如果当前语句影响行数是指定的就成功,否则语句失败。
1 |
|
设置了这几个 hint 后,当前的语句会按照主键(或唯一键)分组,将相同行的请求修改分为一组,分组后仅组内第一条SQL需要抢锁,后续的都不需要申请锁,减少申请锁的流程,然后组内第一条 SQL 已经遍历 B+树査询到数据了,后续组内库存扣减直接改即可,不用再次査询。且组内 SQL 都修改完之后,仅需一次分组提交事务即可。从而实现了将串行处理变成了批处理。
根据阿里云介绍,结合 Inventory hint 单行 TPS 可达 3.1w:

Returning
1 |
|
正常情况下,如果我们 update 扣减了一次库存之后,如果想得知最新的库存,那么需要再执行一次 select 操作,而 Returning 可以直接返回实时的库存减少一次查询。
利用 Returning,我们可以得知实时的库存,发现没库存后,可以直接设置一个标志位,表明秒杀已经结束,快速 fail 请求,降低服务的压力。
幂等性
token
前端按钮防抖虽然能防止用户的重复提交,但是防止不了绕过前端的恶意请求,这个时候可以通过token保证同一时间,同一个用户只能对同一个商品进行有效请求,后端可以通过检测该标识是否已存在,来决定是否创建新订单。
分布式锁
1.以用户维度,加上分布式锁,例如分布式锁的 key 中的内容可以是 xxx+userId ,这样同一个用户的操作会被锁定
2.判断用户是否有在流程中未支付的订单。
3.如果没有则正常进行下单流程
4.如果有则直接返回,提醒前端您还有未支付的订单,请先支付后再继续下单。
这样就能保证用户无法重复下单(恶意占用库存锁单)。
业务手段
预约
例如 Nike 设计就是抢购,预约有一个比较长的时间段,例如 15 分钟。然后预约通过后等待最终抽签结果即可这样的设计通过一段时间的预约,可减少瞬时的压力,再异步通过后台实现抽签来间接解决秒杀的问题。
预售
例如现在的电商活动都搞定金预售
通过下定让用户感觉这个商品已经到手了,不需要再等到双十一或者 618 零点准时抢购,均摊了请求,减少准点抢购的压力。
兜底方案
如果服务压力过大或者代码有漏洞,那么关闭秒杀直接返回秒杀结束,降低服务压力及时止损