You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

87 lines
11 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 06 | 秒杀系统“减库存”设计的核心逻辑
如果要设计一套秒杀系统,那我想你的老板肯定会先对你说:千万不要超卖,这是大前提。
如果你第一次接触秒杀那你可能还不太理解库存100件就卖100件在数据库里减到0就好了啊这有什么麻烦的是的理论上是这样但是具体到业务场景中“减库存”就不是这么简单了。
例如,我们平常购物都是这样,看到喜欢的商品然后下单,但并不是每个下单请求你都最后付款了。你说系统是用户下单了就算这个商品卖出去了,还是等到用户真正付款了才算卖出了呢?这的确是个问题!
我们可以先根据减库存是发生在下单阶段还是付款阶段,把减库存做一下划分。
## 减库存有哪几种方式
在正常的电商平台购物场景中用户的实际购买过程一般分为两步下单和付款。你想买一台iPhone手机在商品页面点了“立即购买”按钮核对信息之后点击“提交订单”这一步称为下单操作。下单之后你只有真正完成付款操作才能算真正购买也就是俗话说的“落袋为安”。
那如果你是架构师,你会在哪个环节完成减库存的操作呢?总结来说,减库存操作一般有如下几个方式:
* **下单减库存**,即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖的情况。但是你要知道,有些人下完单可能并不会付款。
* **付款减库存**,即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。
* **预扣库存**这种方式相对复杂一些买家下单后库存为其保留一定的时间如10分钟超过这个时间库存将会自动释放释放后其他买家就可以继续购买。在买家付款前系统会校验该订单的库存是否还有保留如果没有保留则再次尝试预扣如果库存不足也就是预扣失败则不允许继续付款如果预扣成功则完成付款并实际地减去库存。
以上这几种减库存的方式都会存在一些问题,下面我们一起来看下。
## 减库存可能存在的问题
由于购物过程中存在两步或者多步的操作,因此在不同的操作步骤中减库存,就会存在一些可能被恶意买家利用的漏洞,例如发生恶意下单的情况。
假如我们采用“下单减库存”的方式,即用户下单后就减去库存,正常情况下,买家下单后付款的概率会很高,所以不会有太大问题。但是有一种场景例外,就是当卖家参加某个活动时,此时活动的有效时间是商品的黄金售卖时间,如果有竞争对手通过恶意下单的方式将该卖家的商品全部下单,让这款商品的库存减为零,那么这款商品就不能正常售卖了。要知道,这些恶意下单的人是不会真正付款的,这正是“下单减库存”方式的不足之处。
既然“下单减库存”可能导致恶意下单,从而影响卖家的商品销售,那么有没有办法解决呢?你可能会想,采用“付款减库存”的方式是不是就可以了?的确可以。但是,“付款减库存”又会导致另外一个问题:库存超卖。
假如有100件商品就可能出现300人下单成功的情况因为下单时不会减库存所以也就可能出现下单成功数远远超过真正库存数的情况这尤其会发生在做活动的热门商品上。这样一来就会导致很多买家下单成功但是付不了款买家的购物体验自然比较差。
可以看到,不管是“下单减库存”还是“付款减库存”,都会导致商品库存不能完全和实际售卖情况对应起来的情况,看来要把商品准确地卖出去还真是不容易啊!
那么,既然“下单减库存”和“付款减库存”都有缺点,我们能否把两者相结合,将两次操作进行前后关联起来,下单时先预扣,在规定时间内不付款再释放库存,即采用“预扣库存”这种方式呢?
这种方案确实可以在一定程度上缓解上面的问题。但是否就彻底解决了呢其实没有针对恶意下单这种情况虽然把有效的付款时间设置为10分钟但是恶意买家完全可以在10分钟后再次下单或者采用一次下单很多件的方式把库存减完。针对这种情况解决办法还是要结合安全和反作弊的措施来制止。
例如给经常下单不付款的买家进行识别打标可以在被打标的买家下单时不减库存、给某些类目设置最大购买件数例如参加活动的商品一人最多只能买3件以及对重复下单不付款的操作进行次数限制等。
针对“库存超卖”这种情况在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行锁而并发度越高时等待线程会越多TPSTransaction Per Second即每秒处理的消息数会下降响应时间RT会上升数据库的吞吐量就会严重受影响。
这就可能引发一个问题,就是单个热点商品会影响整个数据库的性能, 导致0.01%的商品影响99.99%的商品的售卖,这是我们不愿意看到的情况。一个解决思路是遵循前面介绍的原则进行隔离,把热点商品放到单独的热点库中。但是这无疑会带来维护上的麻烦,比如要做热点数据的动态迁移以及单独的数据库等。
而分离热点商品到单独的数据库还是没有解决并发锁的问题,我们应该怎么办呢?要解决并发锁的问题,有两种办法:
* **应用层做排队**。按照商品维度设置队列顺序执行,这样能减少同一台机器对数据库同一行记录进行操作的并发度,同时也能控制单个商品占用数据库连接的数量,防止热点商品占用太多的数据库连接。
* **数据库层做排队**。应用层只能做到单机的排队但是应用机器数本身很多这种排队方式控制并发的能力仍然有限所以如果能在数据库层做全局排队是最理想的。阿里的数据库团队开发了针对这种MySQL的InnoDB层上的补丁程序patch可以在数据库层上对单行记录做到并发排队。
你可能有疑问了,排队和锁竞争不都是要等待吗,有啥区别?
如果熟悉MySQL的话你会知道InnoDB内部的死锁检测以及MySQL Server和InnoDB的切换会比较消耗性能淘宝的MySQL核心团队还做了很多其他方面的优化如COMMIT\_ON\_SUCCESS和ROLLBACK\_ON\_FAIL的补丁程序配合在SQL里面加提示hint在事务里不需要等待应用层提交COMMIT而在数据执行完最后一条SQL后直接根据TARGET\_AFFECT\_ROW的结果进行提交或回滚可以减少网络等待时间平均约0.7ms。据我所知目前阿里MySQL团队已经将包含这些补丁程序的MySQL开源。
另外数据更新问题除了前面介绍的热点隔离和排队处理之外还有些场景如对商品的lastmodifytime字段的更新会非常频繁在某些场景下这些多条SQL是可以合并的一定时间内只要执行最后一条SQL就行了以便减少对数据库的更新操作。
## 总结一下
今天,我围绕商品减库存的场景,介绍了减库存的三种实现方案,以及分别存在的问题和可能的缓解办法。最后,我又聚焦秒杀这个场景说了如何实现减库存,以及在这个场景下做到极致优化的一些思路。
当然减库存还有很多细节问题,例如预扣的库存超时后如何进行库存回补,再比如目前都是第三方支付,如何在付款时保证减库存和成功付款时的状态一致性,这些都是很大的挑战。
如果你也有实现减库存的经验或者问题,欢迎留言与我分享。