Skip to content

秒杀系统-设计方案

秒杀系统

“秒杀”是指在有限的时间内对有限的商品数量进行抢购的一种行为,这是商家以“低价量少”的商品来获取用户的一种营销手段。

前端优化

  • 动静分离:CDN、Nginx缓存静态数据

    • CDN(Content Delivery Network,内容分发网络),是一种通过分布式的网络节点,将网站的内容(例如图片、视频、CSS文件等静态资源)缓存到用户附近的服务器上,减少用户访问服务器的物理距离,从而提高网站访问速度的技术。
  • 限制请求频率:例如10 秒钟只能请求 1 次,随后按钮置灰,防止恶意请求

后端优化

  • 增加缓存层+预热数据:进入Redis,将热点数据存入缓存,并在活动开始前提前加载到缓存中,降低数据库层的读取压力。

  • MQ 异步处理:对于非必要的业务逻辑,都通过 MQ 进行异步处理,进一步降低请求处理时间。

  • 限流:预估业务请求量,对请求数量进行限制,当超过限流阈值时,直接拒绝请求,防止系统崩溃。

  • 降级:降级指的是当服务失败或异常后,返回指定的默认信息(系统繁忙,请稍后重试)。

  • 保证高可用:不要单点服务,要多台服务,支持负载均衡、水平扩展(K8S扩容机制 replicas:2)。

  • 限时、限量及用户资格控制:

    • 抢购资格验证:通过验证用户的抢购资格,提前筛选部分用户进入秒杀活动,减少无效请求。

    • 抢购次数限制:每个用户限时限量下单,防止恶意用户刷单。

业务侧优化

一般来说,经过上述的整体优化之后,系统已经能够比较稳当地应对秒杀活动了。如果此时还是流量比较大,那么或许应该从业务侧去进行优化了。(比如增加秒杀活动的时间范围)

  • 例如 12306 刚开始的时候,购买时间都在同一时刻,这导致同一时刻并发量太大,系统经常支撑不住。后来 12306 将购票周期放长,可以提前 20 天购买火车票。通过业务侧的优化,我们将本来在 1 个小时的抢购分摊到了 20 天,服务器压力一下子降低了 480 倍!

  • 例如一个存储了 10 亿条记录的消息记录表,业务侧既想查询速度快,又想进行 1 年数据范围的数据查询,这无论如何都是无法实现的。这时候就需要从业务需求侧进行优化,否则是无法两全其美的。对于这个场景,一个合理的实现方式是:

    • 要实现 1 年数据范围的查询,那么只能根据消息 ID 进行,因为这样可以使用上索引。

    • 而要根据时间范围进行查询,只能缩短查询时间到 3 天内,这样也可以满足业务需求。

扣减库存的问题

一般下单的流程是提交请求,创建订单,扣减库存,处理业务,发起支付。

在扣减库存阶段,我们需要防止“超卖”。

扣减库存的方式

  • 下单扣库存:在用户下单后就扣减库存。(如果用户下单后取消了支付,可能会导致少卖的问题)
  • 支付扣库存:用户付完款后再扣减库存。(如果用户下单后迟迟未支付,那用户就可以一直下单。可能会导致超卖的问题)
  • 下单预扣库存:在用户发起秒杀请求时,首先将库存进行预减,直接在缓存中扣减库存,同时记录到消息队列中。如果最终订单生成失败,可以通过异步消息或定时任务将库存回补。

如何扣减库存防止超卖

  • 数据库层面防止超卖
    • 使用乐观锁Version校验
    • 使用悲观锁for update
  • 利用Redis缓存防止超卖
    • 使用Redis分布式锁
    • Redis库存扣减:将库存数量提前缓存到Redis中,通过原子操作(Lua脚本)进行库存扣减。后续再通过MQ异步同步到MySQL数据库中。
  • 使用消息队列防止超卖
    • 对请求进行削峰处理。将下单请求写入消息队列,下单模块按顺序消费,逐一处理订单。

支付超时的问题

如果用户下了订单,迟迟未支付怎么办?

  • 设置订单失效时间,超时后关闭订单回补库存:下单扣减库存、创建订单成功后,使用RocketMQ发送延迟消息,5分钟后回查订单状态,如果未支付则关闭订单,回补库存。

如果用户下了订单,最后一秒才支付怎么办?或者三方支付系统延迟返回支付成功回调怎么办?

  • 创建订单超过5分钟未支付时,或者没有收到支付成功的回调,订单会被系统自动取消,此时有两种解决方案:
    • 原路退款返回:调用三方支付系统的退款接口,原路返回。
    • 业务优先原则:即促成交易。先校验库存,还有库存的话则再去扣减库存,成功的话则修改订单状态为已支付,正常执行后续流程。
      • 订单取消时会回补库存,所以要先校验库存。

参考

万级高并发秒杀系统设计套路!超详细解读

从全局角度,如何设计一个秒杀系统?