秒杀系统核心设计

March 2, 2022

 

最近阅读量大量关于秒杀系统的介绍文章,包括一些视频讲解,大部分文章都太冗长,没有触及核心设计点,本文主要总结下后台架构的核心设计要点,不讨论架构优化中的常规页面CDN化、隔离部署、数据sharding、缓存优化等常规手段。

背景

秒杀系统场景举例:

  • 抢购:如双十一,下单量高峰能达到50w+ qps,注意是下单成功,同时参与抢购的人的那就更多一个数量级了。
  • 红包:直播间红包、微信红包,本质上都是类似的场景,微信红包的处理方案是提前对红包进行个数拆分,会简化扣除库存逻辑,但是本质上跟抢购类似。

此类系统有几个典型特点:

  • qps高,tps低:即读多写少,大量的请求(包括非正常请求:如频繁重试、黑产请求等)但是只有少量能成交。
  • 库存需要精确:涉及金额,不允许超卖
  • 固定时间:活动时间通常通常很短

其中超卖问题为核心需求点,基于此衍生出如何抗住并发量、用户体验优化等解决方案。

简单方案

先考虑一个简单的实现方案,然后基于此方案的问题进行优化。

存储模型

以DB为例,考虑拆分如下几个表:

  • 秒杀活动信息表: 用于新建秒杀活动,包括活动信息、时间段、秒杀商品等
  • 商品表:存储商品详细信息
  • 库存表stock:活动id、活动库存数量。

其中前两个表的信息为静态信息表,基本无变更。库存表stock会随着秒杀过程会频繁更新,直至库存数量耗尽等。

前端要点

以web为例,描述前端/终端的设计要点:

  • 资源静态化
  • 倒计时

    非活动时间不允许参与,需要考虑本地时间不精确,需要定期从服务端获取倒计时信息。服务端也需要考虑采用TPN等方案保障各个机器时间的一致性。

  • 页面拦截

    避免用户频繁发起重复请求,比如提交、库存耗尽时置灰等手段。减少对后端的冲击。

后端流程

后端接收到秒杀请求后的大致处理流程:

  • 资格校验:包括uid、ip频控等手段,保障抢购公平性的同时也减少后续流量压力,对异常请求进行拦截。
  • 扣减库存 :此过程为重IO逻辑
  • 下单逻辑:包括订单表等相关的一系列一致性操作,此过程的逻辑也很重。
  • 返回订单号,给到前端执行(取消)支付相关流程。

扣减库存

为避免超卖,扣减库存通常有两种解决方案:

  1. mysql锁:mysql并发update问题有两种解决方案
    • 方案一:依赖db操作的原子性,如"update stock set count = count-1 where product_id=xxx and count > 0"
    • 方案二:业务自行上锁,如"begin; select * from stock where product_id = xxx; update xxxx; commit"。
  2. redis + lua:lua解法网上较多,此处就不粘贴了。

这里简单的方案基本就是如上所述,以mysql/redis的性能,支撑个几千的峰值qps并发也没啥问题,核心重点是要考虑好并发可能带来的超卖问题,同时也要保障抢购的公平性。

当然进一步的优化方案也可以基于数据sharding的思路,将库存拆分到多个redis/mysql实例中,以提升系统整体吞吐量。

问题

从上面的后端流程来看,扣减库存、下单逻辑都是相对较重的IO逻辑,很大可能会成为系统的瓶颈。

以mysql为例,假定扣减update的耗时为1ms(数据参考),那每秒支撑的TPS大概在1000左右,超过此数据量时会出现几个问题:

  • 页面502:因为已经到达db极限,接入/逻辑层大概率超时了。
  • 体验问题:假设以排队等方案解决了后端超载的问题,但是大部分用户其实是抢购不到的,但是页面也只能一直轮询(或者长链接等待超时),体验感较差。
  • 风险高:下单逻辑通常会比较复杂,需要保证数据不丢失等,也是一个重IO的逻辑,放在同步流程中非常危险。

整体看同步处理大量的请求是一个不好的解决方案,需要针对性优化。

优化思路

基于前面的问题,优化思路有两点:

  • 及早返回,降低资源占用
  • 同步改异步,提升稳定性,降低风险

将整体流程拆分为2段(不包括支付流程), 对抢到秒杀资格的用户和被拦截的用户区分处理:

  • 抢购

    负责资格校验、库存校验等逻辑,此过程逻辑较轻,且未只读操作。

    此过程同步处理,除了对异常用户的校验外,引入总库存作为拦截手段,对超过库存总量的请求不再处理,及时返回已售罄信息给到前端,避免无意义等待和重试。

    获得抢购资格的用户只有少量,大部分用户无法获得抢购资格,请求会被及时拦截,减少了服务端资源的消耗,也解决了这部分用户的体验问题。

    对于活动抢购资格的用户返回给前端,页面进入轮询查询获取订单信息阶段。这部分用户数量约等于库存数量,并发影响可控。

  • 下单

    负责实际扣减库存、写订单表等重IO逻辑。

    获得抢购资格的用户,写入mq供下单逻辑消费处理,此处可以根据存储层的压力,动态调整实际消费者实例数量,影响可控。获得抢购资格的用户也是需要轮询等待,但是仅影响这部分用户的体验。

    下单处理完后写入完整订单存储、缓存等,供页面查询及支付使用。

问题:如何保障库存拦截校验精确性

抢购过程如何获得剩余库存的准确性是个问题,但是此处并不需要保障做到精确。因为进入下单流程的用户也不一定能真实的走完下单流程,比如下单失败、活动过期、重复下单等都需要有对应的解决方案。下单失败给到终端做好重试(抢购)或者提示(重复下单)即可。

所以抢购时可以根据库存做一个大致的判断,比如总量的两倍作为库存的整体数据限额,或者频控组件定期同步真实库存数据作为本地拦截标准等。

问题:少卖

如果下单有大量的失败(比如重复下单、下单失败),或者用户放弃支付导致的库存回退,可能会导致抢购过程已经拦截了,而实际库存没有耗尽。考虑两种解决方案:

  • 方案一:前面已经提到了,可以在拦截库存时给予一定的buffer,比如按两倍库存来拦。
  • 方案二:放弃支付,可能在一段时间库存耗尽后又会冒出剩余库存的问题,这里可以引入库存同步机制通知到抢购模块。

参考

See all postsSee all posts