Shopify在博客里晒出一个数字:2025年黑五高峰,平台商家的销售额峰值冲到每分钟510万美元,同比涨了11%。这个数字很容易被读成“看,我们的新架构扛住了这么大的流量”。
但这510万美元是全站GMV,是商家在Shopify上卖东西的总流水,不是Shopify重写的那套库存预留系统本身的吞吐量。这篇工程博客从头到尾没有给出预留服务自己的QPS或延迟基准。业务规模和系统性能,是两件事,读新闻时最容易被顺手焊在一起。
库存预留难在哪
结账时点“完成购买”的那一刻,系统要先给这件商品打个临时锁,几分钟内先别让别人抢走,等支付成功了再正式从库存账本里扣掉。这是两步:Reserve(占位)和Claim(真扣)。
Shopify过去用Redis做占位,DECR一下就锁住一件。Redis扛并发没问题,问题出在Claim那一步——占位在Redis,账本在MySQL,两个系统没法包进同一个事务。支付成功了但库存没扣、或者库存扣了占位没释放,这两种错都可能发生,前者超卖惹客诉,后者漏卖丢订单。
这不是Shopify一家的毛病,是“缓存做并发计数、数据库做账本”这套组合的老结构性问题:两个系统各自原子,合在一起就不原子。
一行一个单位,把抢购变成排队抢座
MySQL 8的SKIP LOCKED给了另一个思路:一件商品10个库存,就开10行,谁抢到没被锁的行就归谁,锁住的行直接跳过,不用排队等。这个设计思路借鉴了37signals做任务队列Solid Queue的方式——本质上是把“抢购”问题当成“任务队列”问题来解,谁先抢到空位谁先走。
《劝学》里有句话:“君子生非异也,善假于物也。”意思是聪明人不是天赋异禀,是善于借用现成的东西。MySQL 8早就有SKIP LOCKED,Shopify没有造新轮子,是把这个原本用来分发任务队列的机制,改嫁到库存预留上。
但一行一单位如果不设上限,热门商品分布在多个仓库时能撑到几十万行,扫描速度会被拖垮。Shopify的解法是给每个“商品+仓库”组合的可用行池设了1000行的上限,用完了后台流程再从账本回填,且用锁保证只有一个事务在补货,别让所有并发请求一起去抢着插入新行。
这是全篇里唯一一个具体到能复用的系统参数,其余的黑五数字都是生意规模,不是技术指标。
真正卡住的地方,不是库存预留本身
Shopify自己在博客里说了一句挺诚实的话:最难的教训不是数据库怎么设计,而是发现真正的瓶颈根本不在他们盯着的那些指标上。
线上压测时吞吐量卡在一个远低于目标的天花板上:预留延迟正常,CPU没跑满,查询也早就调优过。团队给每条SQL语句打上业务标签,在ProxySQL层统计每个业务流程占用连接的时长,才发现结账路径上还有别的代码,长时间占着数据库连接没被优化过——因为它们从来没先撞到过连接上限。库存预留成了“压垮骆驼的最后一根稻草”,但骆驼背上真正的重量在别处。
这其实是个经典的排错陷阱:路灯下面找钥匙,因为那里亮,不代表钥匙真掉在那儿。CPU和延迟都正常,看着一切健康,问题却藏在没人监控的连接占用时长里。清理掉这部分代码之后,主库的读操作降了一半、事务量降了三分之一——这个改动跟库存预留的表结构设计完全无关,却是打开吞吐量天花板的关键一步。
招数是真的,战绩是自报的。
这篇博客里,哪些该信、哪些该等
SKIP LOCKED加有界行池,是一个扎实的工程方案,思路清楚,痛点具体,任何在纠结“要不要引入Redis做库存锁”的团队,都可以直接参考。这部分我愿意打高分。
- 结论.把并发争抢问题交给数据库原生机制,比额外搭一套协调层更省心,这条经验值得复用。
- 风险.
SKIP LOCKED天生有“饥饿”风险——运气差的请求可能反复抢不到空行;1000的池上限是Shopify按闲购峰值估出来的经验值,换成一次全球同款的极端抢购,这个数字是否够用,原文没答案。
预留服务自己的QPS、延迟,迁移前后的对比数字,Shopify一个都没披露。目前也找不到独立的技术社区讨论或第三方复现——这套叙事现在只有Shopify一方在说。工程细节可以照单全收,但“扛住了黑五”这个结论,更准确的说法是:至少这次没暴出问题,至于极限在哪,还没人从外部验证过。
数据库这行有句老话:不出问题不代表设计得对,只代表还没撞到边界。Shopify这次的边界,可能要等下一个更极端的大促才会露出来。
