大型电商网站分布式秒杀系统设计三种姿势( 四 )


劣势:用户体验较差 。用户下单后,不一定会实际付款,假设有 100 件商品,就可能出现 200 人下单成功的情况 。
因为下单时不会减库存,所以也就可能出现下单成功数远远超过真正库存数的情况,这尤其会发生在大促的热门商品上 。
如此一来就会导致很多买家下单成功后却付不了款,购物体验自然是比较差的 。
③预扣库存
优势:缓解了以上两种方式的问题 。预扣库存实际就是“下单减库存”和 “付款减库存”两种方式的结合,将两次操作进行了前后关联,下单时预扣库存,付款时释放库存 。
劣势:并没有彻底解决以上问题 。比如针对恶意下单的场景,虽然可以把有效付款时间设置为 10 分钟,但恶意买家完全可以在 10 分钟之后再次下单 。
小结:减库存的问题主要体现在用户体验和商业诉求两方面,其本质原因在于购物过程存在两步甚至多步操作,在不同阶段减库存,容易存在被恶意利用的漏洞 。
实际如何减库存业界最为常见的是预扣库存 。无论是外卖点餐还是电商购物,下单后一般都有个 “有效付款时间”,超过该时间订单自动释放,这就是典型的预扣库存方案 。
但如上所述,预扣库存还需要解决恶意下单的问题,保证商品卖的出去;另一方面,如何避免超卖,也是一个痛点 。
卖的出去:恶意下单的解决方案主要还是结合安全和反作弊措施来制止 。比如,识别频繁下单不付款的买家并进行打标,这样可以在打标买家下单时不减库存 。
再比如为大促商品设置单人最大购买件数,一人最多只能买 N 件商品;又或者对重复下单不付款的行为进行次数限制阻断等 。
避免超卖:库存超卖的情况实际分为两种 。对于普通商品,秒杀只是一种大促手段,即使库存超卖,商家也可以通过补货来解决 。
而对于一些商品,秒杀作为一种营销手段,完全不允许库存为负,也就是在数据一致性上,需要保证大并发请求时数据库中的库存字段值不能为负 。
一般有多种方案:

  • 一是通过事务来判断,即保证减后库存不能为负,否则就回滚 。
  • 二是直接设置数据库字段类型为无符号整数,这样一旦库存为负就会在执行 SQL 时报错 。
  • 三是使用 CASE WHEN 判断语句 。
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
业务手段保证商品卖的出去,技术手段保证商品不会超卖,库存问题从来就不是简单的技术难题,解决问题的视角是多种多样的 。
一致性性能的优化库存是个关键数据,更是个热点数据 。对系统来说,热点的实际影响就是 “高读” 和 “高写”,也是秒杀场景下最为核心的一个技术难题 。
①高并发读
秒杀场景解决高并发读问题,关键词是“分层校验” 。即在读链路时,只进行不影响性能的检查操作 。
如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求等,而不做一致性校验等容易引发瓶颈的检查操作;直到写链路时,才对库存做一致性检查,在数据层保证最终准确性 。
因此,在分层校验设定下,系统可以采用分布式缓存甚至LocalCache来抵抗高并发读 。
即允许读场景下一定的脏数据,这样只会导致少量原本无库存的下单请求被误认为是有库存的,等到真正写数据时再保证最终一致性,由此做到高可用和一致性之间的平衡 。
实际上,分层校验的核心思想是:不同层次尽可能过滤掉无效请求,只在“漏斗” 最末端进行有效处理,从而缩短系统瓶颈的影响路径 。
②高并发写
高并发写的优化方式,一种是更换 DB 选型,一种是优化 DB 性能,以下分别进行讨论 。
更换 DB 选型:秒杀商品和普通商品的减库存是有差异的,核心区别在数据量级小、交易时间短 。
因此能否把秒杀减库存直接放到缓存系统中实现呢,也就是直接在一个带有持久化功能的缓存中进行减库存操作,比如 redis?
如果减库存逻辑非常单一的话,比如没有复杂的 SKU 库存和总库存这种联动关系的话,个人认为是完全可以的 。
但如果有比较复杂的减库存逻辑,或者需要使用到事务,那就必须在数据库中完成减库存操作 。
优化 DB 性能:库存数据落地到数据库实现其实是一行存储(MySQL),因此会有大量线程来竞争 InnoDB 行锁 。
但并发越高,等待线程就会越多,TPS 下降,RT 上升,吞吐量会受到严重影响 。注意,这里假设数据库已基于上文【性能优化】完成数据隔离,以便于讨论聚焦。


推荐阅读