文章摘要
Shopify将库存预留系统从Redis迁移至MySQL,利用MySQL 8的SKIP LOCKED功能,采用每库存单元一行而非每商品一行的设计,成功应对了黑五期间每分钟510万美元销售额的高并发场景,解决了此前单行数量列无法处理的高竞争问题。
文章总结
好的,没问题。作为一名专业的中文编辑,我将对这篇英文文章进行中文重述,保留核心细节,并删减与主题无关的内容。
标题:Shopify 用 MySQL 替代 Redis 处理库存预留,成功实现规模化扩展 (2026)
核心问题: 在结账环节,当买家点击“完成购买”时,系统必须确保商品仍有库存。处理不当会导致两种严重后果:一是超卖(两个买家买到同一件最后商品),商家需取消订单并承担客服成本;二是少卖(告知买家商品无货,实际却有库存),导致商家损失本应达成的销售。
原有方案及其局限: Shopify 的防超卖系统通过“预留库存”机制运作:支付处理时短暂锁定库存,防止并发结账抢占同一件商品。该系统长期运行在 Redis 上。但 Redis 方案存在根本缺陷:库存预留和库存总账(MySQL)分属两个系统,导致“扣款成功但库存未扣除”或“库存已扣但仍被标记为预留”等错误无法通过原子操作避免。此外,Redis 方案缺乏多仓库感知能力,并增加了维护独立集群的运营成本。
新方案:基于 MySQL 的 SKIP LOCKED 设计
受 37signals 的启发,Shopify 团队决定将库存预留也迁移到 MySQL,以实现统一的数据库策略。核心思路是:为每个可售单元创建一行数据,而非为每个商品维护一个数量字段。例如,一个库存为10的商品,在数据库中有10行数据。预留3件商品,就是在一次事务中“选择”并“移动”3行数据。
实现可扩展性的关键在于 MySQL 8 的 SKIP LOCKED 特性。当一个事务锁定了某些行时,其他事务会跳过这些行,转而获取其他可用行,从而避免了行级锁的争用。
关键设计决策:
- 有界资源池: 为避免为海量库存(如5万件商品)创建海量数据行导致查询缓慢,系统为每个“商品/仓库”组合维护一个上限为1000行的可用资源池。预留操作消耗池中的行,并由一个补充进程从库存总账中补充。1000的上限足以应对闪购等突发流量,同时保证查询性能。
- 复合主键: 使用
(shop_id, inventory_item_id, inventory_group_id, id)作为复合主键,替代自增ID。这减少了 InnoDB 引擎在查询时产生的行锁数量,从2个降低到1个,显著提升了高并发下的吞吐量。 READ COMMITTED隔离级别: 将事务隔离级别从默认的REPEATABLE READ改为READ COMMITTED,避免了在空表上执行SELECT ... FOR UPDATE SKIP LOCKED时产生的间隙锁(gap lock),从而允许补充进程顺利插入新行,防止死锁。- 一致的锁顺序: 统一了预留和认领操作中访问表的顺序,避免了因不同事务以不同顺序锁定表而导致的死锁。
- 批量查询: 使用
UNION ALL将购物车中多个商品的预留查询合并为一次数据库往返,降低了延迟。
真正的瓶颈:数据库连接,而非 CPU
在性能测试中,团队发现吞吐量远低于预期,但 CPU 并未满载,查询也已优化。通过为每个 SQL 语句添加业务进程标签(如 /* conn_tag:checkout_completion */),并在代理层(ProxySQL)进行聚合分析,他们发现:真正的瓶颈是数据库连接池的耗尽。
问题不在于预留操作本身慢,而在于结账流程中的其他代码长时间持有数据库连接,导致预留操作在需要时无连接可用。通过清理和优化这些代码,团队移除了50%的主库读操作和33%的事务。同时,重新评估并调整了 InnoDB 的线程并发数,最终移除了性能天花板。
迁移策略: 采用“影子模式”逐步切换。Redis 和 MySQL 同时运行,Redis 仍作为数据源。验证 MySQL 的正确性和性能后,逐步将数据源切换至 MySQL,并保留回退到 Redis 的“一键开关”。
核心经验总结:
- 重新审视旧决策: 过去不可行的方案(如用 MySQL 处理此类高并发工作负载),随着新特性(如
SKIP LOCKED)的出现可能变得可行。数据库配置也需要随着工作负载和硬件的变化而重新评估。 - 从小处着手并观察: 一个简单的原型(如 Ruby 脚本 + MySQL)和直接观察数据库行为(如锁情况),比依赖复杂的理论或大型系统更有效。
- 瓶颈往往不在你关注的地方: 当性能指标出现矛盾(如低 CPU 但高排队)时,需要对整个请求链路进行监控。答案往往隐藏在“管道”而非“引擎”中。
最终结论: 这个项目的成功并非仅仅让“预留”操作变快,而是让它成为一个“安全的邻居”。预留操作与购物车更新、支付处理、订单创建共享同一个数据库。一个会耗尽连接或长时间持有锁的系统会危及所有其他操作。真正的标准是在不损害数据库整体健康的前提下维持高吞吐量。最终,更可靠的预留机制意味着没有超卖,为商家带来了更多成功的交易。
评论总结
根据评论内容,总结如下:
主要观点与论据:
技术方案争议(评论3、4、11):
- 支持方认为“每单位一行”设计(SKIP LOCKED)解决了库存同步问题,但评论4质疑其引入的“池”机制可能增加同步风险。
- 反对方(评论11)指出,为每个店铺/SKU组合保留1000行是“糟糕设计”,建议改用“购物车/SKU一行”方案,避免复杂回填流程。
- 评论3批评文章疑似AI生成,认为Redis更适合做预订系统,并质疑Shopify工程文化。
政治立场批评(评论1):
- 指出Shopify创始人及COO资助极右翼极端主义,创始人主张“只有富人才能投票”,认为技术文章掩盖了公司政治问题。
简化方案建议(评论7):
- 提出更简单方案:在订单流程中直接扣减库存,通过后台进程处理超时或取消的订单,无需复杂锁定机制。
对文章质量的质疑(评论3、9):
- 评论3认为文章由AI生成,缺乏深度;评论9批评文末AI图片“廉价量产感”。
平衡性总结: - 技术层面:部分评论认可MySQL方案(评论8),但多数认为Redis更优(评论3),或提出更简洁替代方案(评论7、11)。 - 非技术层面:评论1揭露公司政治问题,但其他评论未回应此点,形成观点失衡。 - 文章质量:评论3、9批评AI痕迹,但评论2、6、12认可文章价值(如评论12称“绝对迷人”)。
关键引用保留: - 评论1:“Shopify’s founder and their coo both fund far-right extremism...” - 评论3:“this whole thing was written by AI... redis when used correctly feels like an insanely awesome way to do a reservation system” - 评论11:“not the best design to have 1000 rows for each shopSKU combination... why not just have one row per shopping cartSKU?” - 评论7:“Deduct the reservation from the inventory when the user starts to order... simpler than this approach” - 评论12:“This is absolutely fascinating. I enjoy real life stories like this.”