1. 前端静态页面的设计。秒杀页面上能静态化处理的页面元素,我们都要尽量静态化,这样可以充分利用 CDN 或浏览器缓存服务秒杀开始前的请求。
  2. 请求拦截和流控。在秒杀系统的接入层,对恶意请求进行拦截,避免对系统的恶意攻击,例如使用黑名单禁止恶意 IP 进行访问。如果 Redis 实例的访问压力过大,为了避免实例崩溃,我们也需要在接入层进行限流,控制进入秒杀系统的请求数量。
  3. 库存信息过期时间处理。Redis 中保存的库存信息其实是数据库的缓存,为了避免缓存击穿问题,我们不要给库存信息设置过期时间。
  4. 数据库订单异常处理。如果数据库没能成功处理订单,可以增加订单重试功能,保证订单最终能被成功处理。

关于库存的处理

情况一:令牌库存。存在一个 List 保存 N 个令牌,N 为库存数目。获取数据时,直接从 List 中 pop 出一个数据。

在没有库存后,用户只会拿到 nil。当然这个实现方式只是解决抢库存失败后不用再补偿库存的问题,在我们对业务代码异常处理不完善时仍会出现丢库存情况。

同时,我们要注意 brpop 可以从 list 队列“右侧”中拿出一个令牌,如果不需要阻塞等待的话,使用 rpop 压测性能会更好一些。

这个方案的缺点是,如果库存很多,比如上万个,那么让 Redis 加入这么多数据,会使得 Redis 卡顿。

PS. 如果所在的 Redis 副本挂了,那么新副本不一定同步完成,可能造成库存多发的情况?好像这个和方案无关系,有没有解决办法?
评论中有个类似的,作者的回复:行业出现这个情况是立即停止秒杀活动 OR 降级服务,然后将请求放入队列后做相应补救。另外 redis、mysql 等如果对一致性要求高的话,需要做的代价就特别大,每次切换主后从库都是需要同步主库所有数据进度。想要做好,这里需要有性能很好的两个从库一起同步数据。所以做好前期流量预测,做好压测,预备措施可以准备一些,但是多少都会丢一点。

情况二:库存分成 N 份。将一个库存分成多份,每次从其中一个库存中取数据。若每次只 pop 出一个数据,相对于一个库存 List 会更好些。

但是若可能一次 POP 出多个数据,那么可能存在每个库存中剩余的库存数目都不够,比如需要 10 个库存,但是当前库存剩余 5、4、6 的情况。那么怎么处理呢?此时是拒绝用户下单,还是返还库存呢?这就看产品的设计是怎么样了。

如果是有多少就给多少,那么就直接返回。如果不是,那么还需要将拿出来的库存重新 push 进去。

如果是可以请求多个库存,比如库存 1 请求 2 个,库存 2 请求 3 个,库存 3 请求 5 个,也是一种处理方式,这种使得若一次 pop 出多个数据时,单个服务的处理速度更快,但是请求数目更多。另外,如果库存不足、甚至没有的时候,可能会有很多无效访问的请求。此时可能需要监控后,减少请求数目。

原子操作 + 拆开库存方式实现库存方案时,如何减少库存为 0 后接口缓慢的问题?
秒杀、秒杀。肯定是很快就没库存了,只要分片够均匀,在一个分片查不到就返回‘没有库存’或者‘参与用户太多,稍后再试‘呗。如果是要持续几分钟的才能抢完的,这种级别的流量一般也不用分片。

情况三:Redis + Lua

需要执行脚本不要过于复杂,否则可能 Redis 卡顿。

情况四:CAS 乐观锁

multi-watch-exec 执行,需要 Key 在同一个 slot 中。当并发小的时候,效果好;但是若事务内操作过多,可能引发多次重试。

情况五:自旋互斥超时锁

优点是可以对多线程进行互斥,但是会耗费很多系统资源。

恶意请求/爬虫的处理

这里可以通过 js 对单次请求进行加密签名,并且保证每次秒杀接口入口流程和校验方式每个活动不同,这样能预防对方写一次脚本刷全站,同时这样他的爬虫必须使用我们的脚本计算才能正确请求,提高爬虫的门槛,这样对方就需要可编程浏览器才能模拟用户来抢,这个代价就会大很多,另外要求必须真实绑定手机号用户才可以刷,限制单个用户指定时间内并发请求量,限制一个用户购买量,有未成交的不允许再下单,不用数字代表商品 id,让商品标识非数字连贯等多个组合不断提高技术要求和门槛来降低他们抢的性价比。