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 最终落库的链路。该方案在极端闪购场景下面临以下工程挑战:
- 双写不一致性(Dual-write Inconsistency)
- 库存的真理之源(Source of Truth)在 MySQL,而高并发预留发生在 Redis。
- 网络超时、应用进程崩溃或 MQ 积压都会导致 Redis 计数器与 MySQL 实际库存脱节,导致超卖或有货却提示售罄。
- 单行锁竞争瓶颈(Database Lock Contention)
- 如果放弃 Redis 直接写 MySQL,传统做法是在一条记录中维护一个总库存(例如:
UPDATE inventory SET quantity = quantity - 1 WHERE id = 1)。 - 在闪购时,数万个请求同时争抢同一行数据的排他锁(Exclusive Lock),导致数据库线程池爆满、事务大量超时,系统吞吐量发生断崖式下跌。
3. Shopify 新方案:MySQL 8.0 + Bounded Pools
Shopify 彻底抛弃了 Redis 预留层,将业务收拢回 MySQL 8.0,通过对存储模型和锁机制的创新突破了关系型数据库的并发瓶颈。
💡 核心设计三剑客
① 行级单位模型 (Row-per-unit model)
Shopify 不再将库存存储为一个简单的数字计数器,而是将可销售的库存拆分为多条独立的虚拟行记录。
* 传统做法:商品A | 库存: 100 (所有线程争抢这一行锁)
* Shopify做法:
② 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 调用、外部校验)移出事务外部,确保“快进快出”,大幅提升了连接复用率。