Skip to content

Shopify 高并发库存预留系统架构演进总结

https://shopify.engineering/scaling-inventory-reservations

本文总结了 Shopify 如何淘汰基于 Redis 的旧架构,转而使用 MySQL 8.0 SKIP LOCKED + 有界行池(Bounded Pools) 成功承载全球最大规模闪购(Flash Sale)的工程实践,并与传统的 MQ + Redis + MySQL 扣减方案进行横向对比。


1. 核心架构对比:传统方案 vs Shopify 新方案

在应对高并发库存扣减时,业界常见的“异步双写”方案与 Shopify 的“单数据库行池”方案在数据一致性、吞吐量和复杂度上有着本质区别。

📊 架构特性横向对比

对比维度 传统 MQ + Redis + MySQL 方案 Shopify (MySQL 8 + SKIP LOCKED) 方案
核心思想 空间换时间:Redis 承载高并发预留,MQ 异步削峰,MySQL 最终落库。 分治与消除锁等待:库存数据行池化,利用数据库原生并发特性并行扣减。
数据一致性 弱一致性。存在双写不一致风险,需处理网络抖动导致的 Redis 与 MySQL 数据不一致。 强一致性。完全基于 MySQL 事务,天然具备 ACID 特性,无超卖风险。
系统复杂度 极高。需要维护 Redis 集群、MQ 中间件、分布式事务/对账补偿机制。 极低。回归单一关系型数据库,消除分布式中间件,降低运维成本。
并发瓶颈 极高。Redis 单机原子操作(Lua 脚本)吞吐量极高,瓶颈在后端 MQ 消费与 MySQL 写入。 。依赖 MySQL 8.0 性能。通过消除行锁排队,使数据库吞吐量随 CPU 核心数线性提升。
大促防超卖 依赖 Lua 脚本或分布式锁,若后端异步落库失败,需进行复杂的逆向回滚。 依赖数据库行级锁与条件约束,天然杜绝超卖,失败直接回滚事务。

2. 传统 MQ + Redis + MySQL 方案的痛点

在 Shopify 旧版架构以及业界普遍做法中,通常使用 Redis 扣减库存、MQ 异步通知、MySQL 最终落库的链路。该方案在极端闪购场景下面临以下工程挑战:

  1. 双写不一致性(Dual-write Inconsistency)
  2. 库存的真理之源(Source of Truth)在 MySQL,而高并发预留发生在 Redis。
  3. 网络超时、应用进程崩溃或 MQ 积压都会导致 Redis 计数器与 MySQL 实际库存脱节,导致超卖或有货却提示售罄。
  4. 单行锁竞争瓶颈(Database Lock Contention)
  5. 如果放弃 Redis 直接写 MySQL,传统做法是在一条记录中维护一个总库存(例如:UPDATE inventory SET quantity = quantity - 1 WHERE id = 1)。
  6. 在闪购时,数万个请求同时争抢同一行数据的排他锁(Exclusive Lock),导致数据库线程池爆满、事务大量超时,系统吞吐量发生断崖式下跌。

3. Shopify 新方案:MySQL 8.0 + Bounded Pools

Shopify 彻底抛弃了 Redis 预留层,将业务收拢回 MySQL 8.0,通过对存储模型和锁机制的创新突破了关系型数据库的并发瓶颈。

💡 核心设计三剑客

① 行级单位模型 (Row-per-unit model)

Shopify 不再将库存存储为一个简单的数字计数器,而是将可销售的库存拆分为多条独立的虚拟行记录。 * 传统做法商品A | 库存: 100 (所有线程争抢这一行锁) * Shopify做法

行ID 1 | 商品A | 状态: 可销售
行ID 2 | 商品A | 状态: 可销售
...
行ID 100 | 商品A | 状态: 可销售

SKIP LOCKED 消除锁等待

在 MySQL 8.0 中,Shopify 利用了 SELECT ... FOR UPDATE SKIP LOCKED 语法。 * 传统 FOR UPDATE:若某一行已被并发事务锁住,后续事务必须排队等待其释放。 * SKIP LOCKED:当并发事务发现某一行库存已被锁定时,直接跳过该行,寻找下一行未被锁定的空闲记录。 * 效果:成百上千个下单线程可以同时并发地锁定不同的库存行,互不干扰,锁竞争直接降为零。

③ 有界行池 (Bounded Pools)

如果某件爆款商品有 1,000,000 件库存,直接在数据库中创建百万行记录会导致索引爆炸和查询性能劣化。为此,Shopify 引入了有界行池机制: * 数量截断:在预留表(Reservations Table)中,单个商品在单个仓库下的可用行数被严格限制在 1,000 行以内。 * 动态补充(Refill):当行池中的可用行因用户下单被消耗时,后台异步事务会源源不断地从库存大账本(Ledger Table)中划拨额度,将其“实例化”为新的可用行,始终将行池维持在低维度的健康状态。


4. 关键工程优化与配置

为了让 MySQL 8.0 跑出极致性能,Shopify 还在数据库与应用层进行了深度调优:

  • 隔离级别调整:为了防止高并发下由于范围查询触发 InnoDB 的**间隙锁(Gap Locks)**或临键锁(Next-Key Locks)导致大面积死锁,系统采用了 READ COMMITTED 隔离级别。
  • 缩短连接持有时间:Shopify 发现系统的真正瓶颈往往是数据库连接池(Connection Pool)被耗尽。通过极度精简事务生命周期,将非数据库操作(如 RPC 调用、外部校验)移出事务外部,确保“快进快出”,大幅提升了连接复用率。