Shopify 用 MySQL 替代 Redis 实现库存扩展
Shopify replaced Redis with MySQL for inventory reservations–and it scaled

在 Shopify 的规模下,库存超卖或误报缺货都会带来巨大损失。过去我们依赖 Redis 处理库存预留,但数据分散导致无法保证 ACID 一致性。2025 年黑五高峰期,我们成功将系统迁移至 MySQL,利用 MySQL 8 的 SKIP LOCKED 特性,采用“每单位一行”的设计,结合有界池化策略,实现了高并发下的精准库存控制。真正的瓶颈并非数据库性能,而是连接数耗尽。通过优化事务隔离级别、统一锁顺序及清理冗余查询,我们最终在保持正确性的同时实现了规模化扩展。
最艰难的一课并非关于数据库设计,而是发现真正的瓶颈并非我们观测和测量的对象。
HN 评论区
251- manbash
> 我们不再采用每个商品一行并附带数量列的方式,而是改为每个可售单元占一行。一个有 10 个单位的商品就会对应 10 行。
> 但如果所有库存都按每个单元一行来存储,在规模扩大时会崩溃——一个在 10 个地点共有 50,000 个单位的商品意味着会有 500,000 行,而预留查询在扫描这些行时会变慢。因此,我们维护一个有界可用行池,每个商品/地点组合上限为 1,000 行。预留操作消耗池中的行;一个补充流程则从库存账本中为该池补货。
我是不是该对这种方案感到不安?它似乎是为了降低同步问题出现的概率而引入了一个缓冲池(backoff)。
- codedokode
看起来可能有更简单的解决方案。
1. 当用户开始下单时,从库存中扣除预留量,但在同一事务中也维护一行专门记录进行中的订单流程。
2. 如果订单流程被中止或超时,由后台进程将这些库存归还。
这似乎比上述方案更简单,且无需加锁。尽管他们提出的方案也合理,但肯定有某种原因让他们没选择更简单的流程。构建一个可扩展的 GC 服务并不难,也许他们只是不想把这部分分离出来。
- isignal
这绝对令人着迷。我很喜欢这类真实案例。2013 年我去参加了一场 Node 聚会,当时 Target 刚刚从 PHP 切换到 Node,看到他们的指标并听到他们的策略时,体验与此非常相似。
- bijowo1676
“但最艰难的一课并非关于数据库设计。而是我们发现真正的瓶颈并非我们当时观测和测量的那个。”
- zhivota
为每个 shop*SKU 组合保留 1000 行并不是最好的设计方案。如果候选人在 Shopify 的系统设计面试中提出这种方案,我怀疑他很难通过 Senior+ 职位的筛选。
既然要为每个 shop*SKU 保留 1000 行,为什么不直接为每个购物车*SKU 保留一行呢?
这样单行就能代表单个购物车,并包含同一 SKU 的多个商品信息。
无需使用 1000 行上限和补充流程这种权宜之计(cludge)。与其处理 N 行,你始终只需处理单行。