<audio id="audio" title="06 | 秒杀系统“减库存”设计的核心逻辑" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fb/21/fb4643739acee32c2b5764ffef5d0921.mp3"></audio>

如果要设计一套秒杀系统，那我想你的老板肯定会先对你说：千万不要超卖，这是大前提。

如果你第一次接触秒杀，那你可能还不太理解，库存100件就卖100件，在数据库里减到0就好了啊，这有什么麻烦的？是的，理论上是这样，但是具体到业务场景中，“减库存”就不是这么简单了。

例如，我们平常购物都是这样，看到喜欢的商品然后下单，但并不是每个下单请求你都最后付款了。你说系统是用户下单了就算这个商品卖出去了，还是等到用户真正付款了才算卖出了呢？这的确是个问题！

我们可以先根据减库存是发生在下单阶段还是付款阶段，把减库存做一下划分。

## 减库存有哪几种方式

在正常的电商平台购物场景中，用户的实际购买过程一般分为两步：下单和付款。你想买一台iPhone手机，在商品页面点了“立即购买”按钮，核对信息之后点击“提交订单”，这一步称为下单操作。下单之后，你只有真正完成付款操作才能算真正购买，也就是俗话说的“落袋为安”。

那如果你是架构师，你会在哪个环节完成减库存的操作呢？总结来说，减库存操作一般有如下几个方式：

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

以上这几种减库存的方式都会存在一些问题，下面我们一起来看下。

## 减库存可能存在的问题

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

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

既然“下单减库存”可能导致恶意下单，从而影响卖家的商品销售，那么有没有办法解决呢？你可能会想，采用“付款减库存”的方式是不是就可以了？的确可以。但是，“付款减库存”又会导致另外一个问题：库存超卖。

假如有100件商品，就可能出现300人下单成功的情况，因为下单时不会减库存，所以也就可能出现下单成功数远远超过真正库存数的情况，这尤其会发生在做活动的热门商品上。这样一来，就会导致很多买家下单成功但是付不了款，买家的购物体验自然比较差。

可以看到，不管是“下单减库存”还是“付款减库存”，都会导致商品库存不能完全和实际售卖情况对应起来的情况，看来要把商品准确地卖出去还真是不容易啊！

那么，既然“下单减库存”和“付款减库存”都有缺点，我们能否把两者相结合，将两次操作进行前后关联起来，下单时先预扣，在规定时间内不付款再释放库存，即采用“预扣库存”这种方式呢？

这种方案确实可以在一定程度上缓解上面的问题。但是否就彻底解决了呢？其实没有！针对恶意下单这种情况，虽然把有效的付款时间设置为10分钟，但是恶意买家完全可以在10分钟后再次下单，或者采用一次下单很多件的方式把库存减完。针对这种情况，解决办法还是要结合安全和反作弊的措施来制止。

例如，给经常下单不付款的买家进行识别打标（可以在被打标的买家下单时不减库存）、给某些类目设置最大购买件数（例如，参加活动的商品一人最多只能买3件），以及对重复下单不付款的操作进行次数限制等。

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

## 大型秒杀中如何减库存？

目前来看，业务系统中最常见的就是预扣库存方案，像你在买机票、买电影票时，下单后一般都有个“有效付款时间”，超过这个时间订单自动释放，这都是典型的预扣库存方案。而具体到秒杀这个场景，应该采用哪种方案比较好呢？

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

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

`UPDATE item SET inventory = CASE WHEN inventory &gt;= 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），可以在数据库层上对单行记录做到并发排队。

你可能有疑问了，排队和锁竞争不都是要等待吗，有啥区别？

如果熟悉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就行了，以便减少对数据库的更新操作。

## 总结一下

今天，我围绕商品减库存的场景，介绍了减库存的三种实现方案，以及分别存在的问题和可能的缓解办法。最后，我又聚焦秒杀这个场景说了如何实现减库存，以及在这个场景下做到极致优化的一些思路。

当然减库存还有很多细节问题，例如预扣的库存超时后如何进行库存回补，再比如目前都是第三方支付，如何在付款时保证减库存和成功付款时的状态一致性，这些都是很大的挑战。

如果你也有实现减库存的经验或者问题，欢迎留言与我分享。


