🛍️ Shopify 工程内幕 · 替换 Redis 的 MySQL 逆袭

谁说秒杀必须用 Redis?Shopify 用 MySQL 扛住黑五每分钟 510 万美元!

深度图解 Shopify Engineering 2026 最新重磅实践:彻底告别 Redis 与 MySQL 跨系统分布式事务之痛!借助 MySQL 8 SKIP LOCKED 机制,颠覆传统“一行存数量”锁竞争,独创“一物一行(One row per unit)+ 1000行有界缓冲池”;破除间隙锁死锁,排查真正瓶颈“连接池耗尽”,在黑五巅峰写操作 CPU 稳居 50% 以下!

⚡ 一句话看懂这篇技术复盘的精髓
过去业界总有一个思维定势:“做高并发秒杀和库存预留,必须用 Redis 的 DECR/INCR!”
但在 Shopify 撑起全美 14% 电商的体量下,跨系统双写成了致命梦魇——支付成功后扣 MySQL 总账本(Ledger)与清理 Redis 预留无法放入同一个严格 ACID 事务,极易酿成超卖(多卖给顾客引发退款纠纷)或少卖(误报售罄损失真金白银)。
Shopify 团队返璞归真,基于 MySQL 8 SKIP LOCKED 跳过被锁行的特性,把一个商品的多件库存拆为多行独立记录,用 1000 行有界池杜绝长表扫描;排查出真正的性能杀手并非数据库 CPU 而是 ProxySQL 长事务连接泄漏!最终以纯 MySQL 统一全栈架构,完美承载了黑五巅峰每分钟 $5,100,000 的狂暴交易洪峰!
小蓝:MySQL 单库严格 ACID 总账 (Ledger & Pool)
库存预留(Reserve)与真实核销(Claim)在同一个数据库内完成,零跨系统数据不一致风险。
小橙:SKIP LOCKED 无等待调度器
并发事务取货时自动跳过其他事务正在锁定的行,消除热点行竞争,配合 1000 行有界池保障极速扫描。
小绿:连接溯源与全链路健康 (ProxySQL & Tags)
通过 SQL 注释打标追踪业务长连接,消灭非必要长事务,把珍贵的连接池留给秒级并发事务。
红虚线:跨系统双写与死锁泥潭
Redis 与 MySQL 状态分裂、一行库存排队互锁、空表间隙锁(Gap Lock)导致的死锁风暴。

⛓️ 十级演进思想链:从跨系统双写到单库并发巅峰

演进 01

超卖与少卖的噩梦:Redis 独立集群的致命裂痕

前代痛点:过去库存预留跑在 Redis 上,结账成功后永久扣减写在 MySQL 总账本。两套系统无法包裹进同一个 ACID 事务,遇到网络超时,要么付了款没扣库存(超卖),要么没付款却锁死库存(少卖)。
【双系统分裂:数据无法原子提交】
Redis MySQL 两套系统无 ACID 保证
跨系统调用网络抖动导致状态严重错位
【同舟共济:统一收敛至单数据库】
MySQL Unified DB 预留与总账同一事务 从根源杜绝超卖少卖
单库单事务彻底消除两阶段不一致风险
解决了什么:把秒杀预留(Reserve)与真实扣账(Claim)收敛到同一个 MySQL 数据库内,完全依赖成熟的 ACID 事务保障数据绝对一致。
代价是什么:MySQL 必须独自承担以往由内存 Redis 吸收的恐怖秒杀并发。
演进 02

单行数量的死锁牢笼:传统 UPDATE 排队瓶颈

前代痛点:如果按传统设计,每个商品在表里只有一行记录(`quantity = 100`),几千个并发结账请求同时执行 `UPDATE ... WHERE id = 1`,全部堵在同一行行锁上,耗时直奔几秒甚至死锁崩盘。
【千军过独木桥:单行排队卡死】
row: qty=100 几千个事务抢同一行锁
热点单行数据引发严重的锁等待与超时
【化整为零:一物一行,并发无争】
One Row Per Unit 10 件库存拆为 10 行 各自锁各自的行
将单行标量拆分成独立行记录消除锁竞争
解决了什么:颠覆传统“一商品一行存库存”的设计,借鉴 37signals Solid Queue 思想,转变为“一件可售库存对应一行记录(One row per unit)”。
代价是什么:数据库记录行数显著膨胀,需要精密的表容量治理机制。
演进 03

跳过拥堵行:MySQL 8 SKIP LOCKED 释放无锁并发

前代痛点:即使用了多行记录,如果两个买家都执行 `SELECT ... FOR UPDATE LIMIT 1`,第二个事务依然会停下来苦等第一个事务提交,性能依然上不去。
【苦苦等待:遇锁即阻塞】
FOR UPDATE 阻塞 前面不交后面不动
普通锁定读取会导致并发线程剧烈阻塞
【绕道直取:被锁直接跳过】
FOR UPDATE SKIP LOCKED 有人占了这行?跳过! 零等待秒速拿到可用行
自动跳过已被锁定的行实现零等待并发
解决了什么:引入 MySQL 8 的 `SKIP LOCKED` 特性,事务遇到已被其他并发请求持锁的行直接忽略,顺延锁定下一行空闲记录,完全没有阻塞等待。
代价是什么:该语法依赖 MySQL 8.0+ 现代化内核,低版本数据库无法支持。
演进 04

防御长表扫描膨胀:1000 行有界缓冲池设计

前代痛点:如果一个爆款商品在 10 个仓库有 50,000 件库存,全拆成行就是 50 万行!执行 `SKIP LOCKED` 扫描 50 万行直接把数据库 IO 与 CPU 拖垮。
【全量膨胀:几十万行深渊扫描】
500,000 行大表 扫描巨慢拖垮磁盘
全量拆分导致扫描表体积过大延迟飙升
【有界缓冲:上限 1000 行轻装上阵】
Bounded Pool (1000) 预留池恒定精简 后台异步自动补仓
严格设定千行有界池保障扫描始终微秒级
解决了什么:设立“有界缓冲池(Bounded Pool)”,每个商品/仓库只在预留表维护最多 1000 行记录,消耗后由补充进程向总账异步批量 refill,扫描极快。
代价是什么:需要设计当 1000 行瞬间被秒光时的同步排队补仓兜底机制。
演进 05

主键与锁的微观战争:复合主键砍掉一半行锁

前代痛点:原型阶段使用自增 ID 主键,用 `SHOW ENGINE INNODB STATUS` 抓包惊呆了:每预留 1 行居然触发了 2 个行锁(锁定二级索引 + 聚簇索引主键)!
【双重加锁:自增主键累赘】
Auto-inc ID 锁二级索引+聚簇主键
一次锁定两个索引结构导致锁数量翻倍
【精准命中:复合主键单锁合一】
PK(shop, item, group, id) 查询过滤直接走主键 每行只锁 1 次,吞吐暴增
复合主键让过滤条件与聚簇索引完美合体
解决了什么:将过滤条件(shop_id, inventory_item_id 等)直接做进复合主键,让 InnoDB 在定位数据时仅需在聚簇索引上加 1 个行锁。
代价是什么:复合主键长度变长,二级索引(如有)的存储占用会有轻微增加。
演进 06

破除幽灵间隙锁:切换 READ COMMITTED 终结死锁

前代痛点:在 MySQL 默认的 REPEATABLE READ(可重复读)下,当池子被秒空执行锁定查询时,InnoDB 会加上恶名昭彰的间隙锁(Gap Lock/Supremum),导致后面的补货 INSERT 无法写入并频繁死锁!
【间隙封死:空表死锁风暴】
Gap Lock / Supremum 锁住空白区间,写入死锁
默认隔离级别的间隙锁阻止了补货写入
【读已提交:无缝安全补仓】
READ COMMITTED 彻底消除 Gap Lock 补货 INSERT 畅通无阻
降为读已提交消除间隙锁杜绝补货死锁
解决了什么:破天荒在该事务中将隔离级别改为 READ COMMITTED,消除了空表上的 Gap 间隙锁,让后台补货能立即写入。
代价是什么:应用框架必须支持为单特定事务动态定制隔离级别。
演进 07

锁顺序标准化:杜绝跨表循环死锁

前代痛点:预留(Reserve)先往已预留表 INSERT 再从可用池 DELETE;而核销(Claim)又在动已预留表,两个并发事务如果反向加锁,瞬间构成环路死锁。
【方向冲突:跨表交叉等待】
A先改表1再表2 B先改表2再表1 ➔ 死锁
不同业务路径加锁顺序不一致产生环路死锁
【严格单向:标准加锁铁律】
先可用,后预留 单向依赖永不回环
严格规范所有路径加锁次序消灭环路死锁
解决了什么:规范所有业务路径必须遵循一致的加锁顺序:统一先 DELETE 可用池表,再 INSERT 预留记录表;核销只动预留表,物理切断死锁闭环。
代价是什么:团队所有编写库存相关 SQL 的工程师必须严格执行该工程纪律。
演进 08

购物车批量打包:UNION ALL 击碎网络往返延迟

前代痛点:结账购物车里经常有 5 件不同商品,如果发起 5 次独立的数据库网络往返(RTT),数据库连接被长久占据,累积延迟难以承受。
【来回折腾:多次单件往返】
5 次查询 ➔ 5 次网络 RTT 长久占用昂贵连接
单购物车多次往返极度消耗连接时间
【一网打尽:UNION ALL 一次搞定】
Query A UNION ALL B 单次请求锁定全部行 网络开销大幅骤降
合并为单次SQL批量获取多商品预留
解决了什么:采用 `UNION ALL` 拼接多个商品的 SKIP LOCKED 子查询,一次网络往返锁定整辆购物车的全部库存。
代价是什么:拼接 SQL 相对较长,需由底层框架层封装生成。
演进 09

抓出真正元凶:ProxySQL 注释打标追踪连接泄漏

前代痛点:所有优化做完后压测依然受挫:CPU 还没跑满,线程却开始排队,ProxySQL 连接池全面告急!到底是谁占着茅坑不拉屎?
【两眼一抹黑:连接池耗尽不知情】
连接池爆满但 CPU 空闲 无法定位具体慢调用者
只知道连接用尽却无法归因到具体业务
【精准破案:/* conn_tag:xxx */】
/* conn_tag:checkout */ ProxySQL 聚合耗时排行 揪出无关长事务元凶
SQL打标溯源让非必要长连接无处遁形
解决了什么:在 SQL 中注入 `/* conn_tag:业务名 */`,在中间件 ProxySQL 解析聚合;震惊地发现库存预留根本不慢,是结账路径其他非核心模块长久霸占连接!清理后消除了主库 50% 读与 33% 事务!
代价是什么:需要在应用层统一集成 SQL 注释注入与中间件日志管道。
演进 10

黑五终极大考:影子双写验证与纯 MySQL 胜利

前代痛点:直接在黑五秒杀前把几十万商家的核心库存从 Redis 切到新架构,一旦翻车就是数亿美元的灾难。
【豪赌式切换:风险不可控】
一刀切切换系统 出现纰漏全网暴毙
传统一刀切迁移面临极高业务翻车风险
【暗流验证:影子双写 + 灰度切流】
Shadow Mode 灰度切流 黑五分钟销售额 $510 万 写 CPU 稳居 <50%
影子双写万无一失纯MySQL抗住黑五峰值
解决了什么:通过“影子模式(Shadow Mode)”双写验证真实业务结果一致性,平滑灰度切流,黑五巅峰每分钟 $510 万美元销售额下,写入 CPU 稳居 50% 以下!
代价是什么:双写过渡期需要承担少量的双倍系统资源开销。

⚖️ Shopify 库存预留架构的四大核心黄金天平

颠覆思维定势的背后,是极其清醒的工程权衡哲学:

一致性底线 (ACID Atomicity)

宁要单库不要分布式:坚决杜绝跨系统无法单步提交的 Redis+MySQL 分裂架构,宁可优化关系库,也要把预留与总账焊进同一个事务。

有界缓冲防御 (Bounded Contention)

限制有界大小:一物一行虽好,但表不能无限膨胀。1000 行有界池完美平衡了瞬间秒杀吸收力与微秒级扫描性能。

锁颗粒度极致克制 (Lock Minimization)

消灭冗余锁:复合主键砍掉二级索引行锁,READ COMMITTED 消除间隙锁,严格单向加锁次序杜绝死锁。

连接管道生命线 (Connection Stewardship)

做优秀的数据库邻居:真正的瓶颈往往是连接池而非 CPU,杜绝长事务占用,把毫秒级连接让渡给核心并发结账链路。

📋 旧方案 (Redis + MySQL) vs 新方案 (纯 MySQL SKIP LOCKED) 终极全景对照

对比维度 旧架构 (Redis 预留 + MySQL 异步总账) 新架构 (纯 MySQL 8 SKIP LOCKED 有界池)
🛡️ 事务一致性保证 跨系统分布式双写,无法实现单步原子事务,易超卖/少卖。 单库强 ACID 保证,预留与核销同一事务,超卖/少卖概率彻底归零。
📍 多仓位感知能力 (Multi-Location) Redis 键值模型难以直接表达复杂的跨多仓库库存履约规则。 原生关系型关联,复合主键直接携带 location_id,轻松多仓分流。
⚡ 锁竞争与等待模型 Redis 依靠单线程内存原子操作,但向 MySQL 同步时面临排队。 SKIP LOCKED 无等待穿透,多事务并发取不同行,互不干扰。
🏗️ 运维基础设施成本 必须维护昂贵的高可用 Redis 集群、哨兵与数据同步中继。 统一收敛至已有 MySQL 集群,无额外跨系统架构运维负担。
🔥 真实极端峰值表现 (黑五) 容易在高峰期由于双写状态漂移引发客服大量人工退单纠纷。 黑五每分钟 $510 万美元巅峰下,主库写 CPU 稳居 50% 以下!

🗺️ 官方权威参考与技术路线