7. 秒杀系统-- “减库存” 设计的核心逻辑

减库存的几种方式

总结来说,减库存操作一般有如下几个方式:

  • 下单减库存,即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖的情况。但是你要知道,有些人下完单可能并不会付款。
  • 付款减库存,即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。
  • 预扣库存,这种方式相对复杂一些,买家下单后,库存为其保留一定的时间(如 10 分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买。在买家付款前,系统会校验该订单的库存是否还有保留:如果没有保留,则再次尝试预扣;如果库存不足(也就是预扣失败)则不允许继续付款;如果预扣成功,则完成付款并实际地减去库存。

减库存可能存在的问题

       由于购物过程中存在两步或者多步的操作,因此在不同的操作步骤中减库存,就会存在一些可能被恶意买家利用的漏洞,例如发生恶意下单的情况。

下单减库存

       即用户下单后就减去库存,正常情况下,买家下单后付款的概率会很高,所以不会有太大问题。但是有一种场景例外,就是当卖家参加某个活动时,此时活动的有效时间是商品的黄金售卖时间,如果有竞争对手通过恶意下单的方式将该卖家的商品全部下单,让这款商品的库存减为零,那么这款商品就不能正常售卖了。要知道,这些恶意下单的人是不会真正付款的,这正是“下单减库存”方式的不足之处。

付款减库存

      采用“付款减库存”的方式可以解决 ”下单减库存中“ 存在的恶意下单行为。但是会导致另一个问题即:库存超卖。假如有 100 件商品,就可能出现 300 人下单成功的情况,因为下单时不会减库存,所以也就可能出现下单成功数远远超过真正库存数的情况,这尤其会发生在做活动的热门商品上。这样一来,就会导致很多买家下单成功但是付不了款,买家的购物体验自然比较差。

预扣库存

      预扣库存就是将上面的两种方法进行了结合,将两次操作进行前后关联起来,下单时先预扣,在规定时间内不付款再释放库存。这种方案确实可以在一定程度上缓解上面的问题,但却并没有彻底的解决,针对恶意下单这种情况,虽然把有效的付款时间设置为 10 分钟,但是恶意买家完全可以在 10 分钟后再次下单,或者采用一次下单很多件的方式把库存减完。针对这种情况,解决办法还是要结合安全和反作弊的措施来制止。针对恶意下单这种情况,虽然把有效的付款时间设置为 10 分钟,但是恶意买家完全可以在 10 分钟后再次下单,或者采用一次下单很多件的方式把库存减完。针对这种情况,解决办法还是要结合安全和反作弊的措施来制止。

库存超卖

       针对此情况,在 10 分钟时间内下单的数量仍然有可能超过库存数量,遇到这种情况我们只能区别对待:对普通的商品下单数量超过库存数量的情况,可以通过补货来解决;但是有些卖家完全不允许库存为负数的情况,那只能在买家付款时提示库存不足。

大型秒杀中的减库存设计

       目前来看,业务系统中最常见的就是预扣库存方案,像你在买机票、买电影票时,下单后一般都有个“有效付款时间”,超过这个时间订单自动释放,这都是典型的预扣库存方案。

       由于参加秒杀的商品,一般都是“抢到就是赚到”,所以成功下单后却不付款的情况比较少,再加上卖家对秒杀商品的库存有严格限制,所以秒杀商品采用“下单减库存”更加合理。另外,理论上由于“下单减库存”比“预扣库存”以及涉及第三方支付的“付款减库存”在逻辑上更为简单,所以性能上更占优势。

       下单减库存”在数据一致性上,主要就是保证大并发请求时库存数据不能为负数,也就是要保证数据库中的库存字段值不能为负数,一般我们有多种解决方案:一种是在应用程序中通过事务来判断,即保证减后库存不能为负数,否则就回滚;另一种办法是直接设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时会直接执行 SQL 语句来报错;再有一种就是使用 CASE WHEN 判断语句,例如这样的 SQL 语句:

UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END

秒杀减库存的极致优化

       在交易环节中,“库存”是个关键数据,也是个热点数据,因为交易的各个环节中都可能涉及对库存的查询。但是,我在前面介绍分层过滤时提到过,秒杀中并不需要对库存有精确的一致性读,把库存数据放到缓存(Cache)中,可以大大提升读性能。解决大并发读问题,可以采用 LocalCache(即在秒杀系统的单机上缓存商品相关的数据)和对数据进行分层过滤的方式,但是像减库存这种大并发写无论如何还是避免不了,这也是秒杀场景下最为核心的一个技术难题。

       秒杀商品和普通商品的减库存还是有些差异的,例如商品数量比较少,交易时间段也比较短,因此这里有一个大胆的假设,就是把秒杀商品减库存直接放到缓存系统中实现,也就是直接在缓存中减库存或者在一个带有持久化功能的缓存系统(如 Redis)中完成。如果你的秒杀商品的减库存逻辑非常单一,比如没有复杂的 SKU 库存和总库存这种联动关系的话,这种方案是可行的。但是如果有比较复杂的减库存逻辑,或者需要使用事务,还是必须在数据库中完成减库存。

      由于 MySQL 存储数据的特点,同一数据在数据库里肯定是一行存储(MySQL),因此会有大量线程来竞争 InnoDB 行锁,而并发度越高时等待线程会越多,TPS(Transaction Per Second,即每秒处理的消息数)会下降,响应时间(RT)会上升,数据库的吞吐量就会严重受影响。这就可能引发一个问题,就是单个热点商品会影响整个数据库的性能, 导致 0.01% 的商品影响 99.99% 的商品的售卖,这是我们不愿意看到的情况。一个解决思路是遵循前面介绍的原则进行隔离,把热点商品放到单独的热点库中。但是这无疑会带来维护上的麻烦,比如要做热点数据的动态迁移以及单独的数据库等。而分离热点商品到单独的数据库还是没有解决并发锁的问题,此时可以采用以下这两种方法:

  • 应用层做排队。按照商品维度设置队列顺序执行,这样能减少同一台机器对数据库同一行记录进行操作的并发度,同时也能控制单个商品占用数据库连接的数量,防止热点商品占用太多的数据库连接。
  • 数据库层做排队。应用层只能做到单机的排队,但是应用机器数本身很多,这种排队方式控制并发的能力仍然有限,所以如果能在数据库层做全局排队是最理想的。阿里的数据库团队开发了针对这种 MySQL  的 InnoDB 层上的补丁程序(patch),可以在数据库层上对单行记录做到并发排队。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值