# Shopify 将库存预留移至 MySQL

Published: 2026-10-10

Shopify 将其库存预留系统从 Redis 迁移到已经包含库存分类账的 MySQL 数据库。一个有界池中的独立可锁定单位行允许并发结账使用 SKIP LOCKED 来选择不同的合格单位。该设计还依赖于事务边界、主键布局、补货规则和观察整个结账过程中的连接保持时间。

Canonical: https://thedailydiff.dev/zh-CN/video/shopify-reservation-locks/

## 本视频涵盖的内容

- 旧的 Redis 数量计数器模型处理了并发，但预留清理和 MySQL 分类账更新无法共享一个单一的本地原子事务。
- 替代方案对每个可用单位使用一行，每个商品/位置组合的池上限为 1,000。空池可以触发内联补货，并发请求在补货锁后面等待。
- 复合主键是 (shop\_id, inventory\_item\_id, inventory\_group\_id, id)。Shopify 在其原型中减少了锁开销，并使用 READ COMMITTED 来避免阻塞补货的间隙锁。
- 预留先删除池行，然后插入预留记录。提交会释放数据库锁，同时存储的保留会在支付过程中持续；成功支付会在后续的原子事务中认领分类账并移除预留。
- MySQL 将 SKIP LOCKED 描述为一个不一致的视图，它会省略锁定的行。它既不能建立完整的库存计数，也不能公平轮流，仅适用于行级锁，并且对基于语句的复制不安全。
- 源示例包含 expires\_at，但 Shopify 没有记录 MySQL 的过期清理算法。视频中的释放/过期分支是一个解释性的生命周期要求。
- 其他结账代码中的连接保持时间是最终的吞吐量瓶颈。Shopify 对两个系统进行影子写入，比较结果，并逐步切换，同时设置了 Redis 的紧急停止开关作为回退。

## 章节

- 0:00 为什么将预留移至 MySQL？
- 0:29 为什么两个存储系统有风险？
- 0:52 什么成为可锁定单元？
- 1:15 当池耗尽时会发生什么？
- 2:14 SKIP LOCKED 遗漏了什么？
- 2:26 真正的瓶颈在哪里？
- 2:38 我为什么要推出这个设计？

## 翻译的文字记录

译自英文原版旁白。可用音频和字幕由 YouTube 控制。

### 为什么将预留移至 MySQL？

0:00 结账需要 Redis 来保持快速。 Shopify 将库存预留移至 MySQL，在有界池中为每个单元使用一行。 为什么选择 MySQL？ 如何安全地跳过锁？ 这是 The Daily Diff，幕后揭秘。 为什么快速查询仍然触及天花板？ Shopify 是一个用于在线和实体销售的商务平台。 Redis 是一个内存数据存储。

0:21 MySQL 是一个关系型数据库，Shopify 已经将库存分类账保存在 那里。预留在买家付款时短暂保留库存。

### 为什么两个存储系统有风险？

0:29 他们的 Redis 模型会递减一个商品计数器。 认领已付款订单意味着更新 MySQL 分类账并清理 Redis。 这些独立写入可能导致库存重复销售或在应该可售时却不可用。 旧模型也缺乏位置感知。 替代方案必须从某个可以履行订单的地方选择库存。 一个在错误大陆的仓库是一个出色的数据库条目，但一个糟糕的 配送承诺。 早期的 MySQL 尝试使用数量行，因此竞争的结账会在同一个锁处排队。

### 什么成为可锁定单元？

0:56 想象一下电子表格单元格周围的一条天鹅绒绳索。 增加更多工人只会延长队列。 Shopify 改变了可锁定对象。 每个可用单元都有自己的一行。 一个锁定读取会跳过其他事务 持有的单元，并选择其他合格的 单元。不同的工人可以获取不同的行， 同时热点计数器不会妨碍他们。

### 当池耗尽时会发生什么？

1:15 该池对每个商品和位置限制为一千行。 补货从分类账中抽取。 如果它耗尽，预留 路径会内联补货，竞争请求 在补货锁后面等待。 空池不代表空仓库。 预留会删除选定的池行，然后在事务中插入预留记录。 提交会释放数据库锁。

1:35 回滚会撤销更改。 预留作为存储状态在支付处理过程中存活。 成功支付会原子性地认领分类账并移除预留。 数据库锁永远不需要看管支付表单。 他们的复合主键以商店、商品、 组，然后是单元标识开始。 匹配查找减少了其原型中的索引锁定。 他们还使用已提交读（READ COMMITTED）来避免阻塞池

1:57 补货的间隙锁，并使用一致的表顺序来防止循环等待。 发布的示例记录了过期时间。 废弃的支付最终需要释放库存，否则购物车就会变成 一个房东。Shopify 的帖子没有明确说明该清理算法， 所以这张图显示了生命周期要求。 问题来了。

### SKIP LOCKED 遗漏了什么？

2:14 SKIP LOCKED 排除锁定的行，所以手册称其结果为不一致的 视图。它既不能提供完整的库存计数，也不能公平轮流。 保持可用性决策和补货规则围绕它。

### 真正的瓶颈在哪里？

2:26 那个瓶颈呢？ 其他结账代码持有数据库连接时间过长。 Shopify 标记了调用者并测量了连接保持时间， 然后清理了结账路径并重新审视了线程并发。 快速查询仍然可以在查询之外排队。

### 我为什么要推出这个设计？

2:38 他们对两个系统进行了影子写入，以 Redis 为权威， 比较结果，然后通过紧急停止开关逐步切换。 我的判断是 SHIP IT。 我将推出共享事务边界和可回滚的推出方案， 同时对整个结账路径进行监测。 对此有什么问题吗？ 请在评论中提出。 这就是今天的 The Daily Diff。

2:54 我是来自 Axrisi 的 Niko。 负责任地合并。

## 来源

- [We replaced Redis with MySQL for inventory reservations—and it scaled](https://shopify.engineering/scaling-inventory-reservations) — Shopify Engineering — Emilie Noel
- [Simplified reservation SQL embedded in Shopify's article](https://gist.github.com/CourtneySymons/cb5ecbe86331047aae166d5b1f1d555c) — Shopify Engineering / CourtneySymons on GitHub Gist
- [MySQL 8.0 — Locking Reads](https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html) — Oracle / MySQL Reference Manual
- [MySQL 8.0 — Transaction Isolation Levels](https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html) — Oracle / MySQL Reference Manual
- [What Is Shopify and How Does It Work?](https://www.shopify.com/blog/what-is-shopify) — Shopify
- [Redis quick starts](https://redis.io/docs/latest/develop/get-started/) — Redis documentation
- [What is MySQL?](https://dev.mysql.com/doc/refman/8.0/en/what-is-mysql.html) — Oracle / MySQL Reference Manual
