限时秒杀活动上线前,技术群里最常见的一句话是:“库存表就一条记录,别整那么多花活,直接 UPDATE 一把梭。”说这话的人,往往没经历过库存数据变成负数的那一刻。我经历过。2021年帮一家快消品客户做618大促压测,他们的秒杀接口在预演时表现极佳,单机TPS轻松破千,结果活动当天前10秒就收到库存异常告警,数据库里的库存余量变成了-7。不是并发不够,恰恰是并发太猛,猛到把“看似正确”的SQL打穿了。
这篇文章不聊CDN,不聊MQ削峰,不聊前端秒杀按钮置灰,只聊一件事:数据库里那一行 stock 字段,在限时秒杀流量洪峰下,怎么才能做到“精准减少、绝不超卖、尽量少锁”。我会把从普通 UPDATE 到条件更新,再到缓存预扣减与对账补偿的完整演进路径拆开,结合我实际压测过的数据和踩过的坑,给出你在不同业务量级下可以直接抄作业的方案。
先给结论,后文再展开论证。秒杀库存数据精准把控的本质,不是追求某个技术方案的绝对完美,而是保证“扣减流水”与“库存余量”在任何时刻都满足数据守恒定律:期初库存=已售数量+可用库存+冻结库存(未支付订单占用)。任何一秒出现超卖,本质是“已售数量”的增加没有经过原子校验;任何一秒出现少卖,本质是“冻结库存”没有按期释放回补。
围绕这条守恒定律,我测试过四类主流方案的极限表现,这里先把压测数据摆出来(MySQL 8.0,单表300万商品数据,8核16G云主机,500并发线程持续压测60秒):
| 方案 | 稳定QPS | 错误率(超卖/少卖) | 平均响应时间 | P99响应时间 |
|---|---|---|---|---|
| 普通 UPDATE(无校验) | 约 850 | 超卖严重,无法接受 | 35ms | 180ms |
| 乐观锁(version字段) | 约 320 | 不超卖,但失败率极高 | 42ms | 210ms |
| 悲观锁(SELECT FOR UPDATE) | 约 450 | 不超卖,锁等待严重 | 55ms | 340ms |
| 条件更新(WHERE stock > 0) | 约 1200 | 不超卖 | 25ms | 120ms |
这张表是本文最核心的判断依据。条件更新的性能远超悲观锁与乐观锁,且天然防超卖。大多数人认为“Redis是秒杀标配”是个误区,对于大部分中小型电商,一次正确的数据库条件更新,比引入一套缓存架构更高效、更可靠。
为什么条件更新性能最好?因为它把“检查库存是否足够”和“扣减库存”合并成一条原子SQL,不产生额外的版本号比较开销,也不持有长时间的行锁。

在我服务的客户中,遇到最多的不是“技术不会”,而是“技术选型与业务场景错配”。我复盘一次真实的秒杀活动事故,让你看到库存数据是怎么一步步走向失控的。
客户在某平台做“5万份小样0.01元秒杀”,活动前把100个SKU的库存统一灌入一张 goods_stock 表。秒杀开始后,前端流量在5秒内从200QPS暴增到6000QPS,但服务层接口只做了粗略的分布式限流,数据库连接池最大连接数40。结果,大量请求同时执行以下逻辑:
SELECT stock FROM goods_stock WHERE sku_id = ?UPDATE goods_stock SET stock = stock - 1 WHERE sku_id = ?三步逻辑在并发下互相踩踏。多个请求同时读到“剩余1件”,同时通过判断,同时执行扣减,最终数据库里的库存被扣成负数。那一天,该品牌在20分钟内产生了4000多张超卖订单,后续处理退款与赔偿花了整整一周。
超卖问题肉眼可见,少卖问题则隐蔽得多。另一个客户做“限时前100名半价”活动,为了让用户感觉“库存紧张”,运营把款式A的库存由50件改成5件。活动结束后,数据对账发现实际售出3件,但后台库存显示仍为2件,看起来没问题。问题出在另一个场景:当某用户把款式A加入购物车但未支付,订单表里生成了一条“待支付”记录,同时引发了一次预扣库存操作。
这个用户最后没有付款,但预扣的库存没有释放。后台库存显示“还剩2件”,真实可售库存其实是3件。“用户看到有货、下单失败”与“后台显示无货、实际有货”是两种截然不同的库存事故,但根源相同:缺少回补机制。
一次完整的秒杀库存流转,包含以下七个关键节点,任何一个节点处理不当,都会导致库存数据失真:
其中第3步和第5步是对数据库压力最大的两个节点,也是文章后续要重点深入的部分。

写这篇文章之前,我特意搜索了目前搜索引擎里“数据库存秒杀库存适配”相关的高排名内容,发现一个现象:排名靠前的文章中,竟然没有一篇真正深入讲解数据库层面的库存扣减细节,大多停留在“引入Redis”“使用消息队列”“加乐观锁”这种泛泛而谈的层面。这恰好给了我们做深度内容的机会。
这句话只在极端流量下成立。以我压测过的条件更新方案为例,单表在8核16G的云服务器上能稳定支撑1000+QPS的扣减操作。对于绝大多数SKU数量在100以内、总库存10万以内的中小型秒杀活动,数据库完全扛得住。Redis的价值不是“必须”,而是“可选的加速器”和“流量缓冲层”。它的引入会带来新的问题:缓存与数据库的一致性问题、缓存雪崩、缓存击穿。在库存精准把控这件事上,Redis做得不好反而会制造新的数据黑洞。
乐观锁确实不会超卖,但它引入了一个被严重低估的副作用:大量无意义的更新失败与重试风暴。秒杀场景下,同一个SKU会被成千上万个请求同时命中,乐观锁的 version 字段会让超过99%的更新请求失败。客户端一看失败就重试,重试又全部失败,数据库连接资源被白白耗尽,系统整体吞吐量断崖式下跌。上面那组测试数据中,乐观锁方案的稳定QPS只有320,只有条件更新方案的四分之一。
悲观锁(SELECT FOR UPDATE)的“慢”不是线性变慢,而是指数级恶化。我在压测中观察到:当并发超过200时,锁等待时间从个位数毫秒暴涨到300毫秒以上,而且锁等待会导致连接池连接被占满,新请求全部排队,最终引发数据库连接池耗尽。更可怕的是,如果事务中不小心混入了远程调用(如调用支付接口、发送短信通知),锁的持有时间会被拉长到秒级,整个秒杀活动直接雪崩。
这是最隐蔽的误区。SQL写法正确只是第一步。库存精准把控是一个“数据守恒”系统工程,还包含:订单超时回补、支付结果对账、缓存与数据库纠偏、库存操作监控。我见过太多团队把条件更新写好就上线,结果在支付回调环节出现重复扣减,或者在订单取消环节丢失回补,导致库存一点点泄漏。SQL只是防线中的一道,数据守恒需要每一道防线都完备。
把四个误区的技术要点和适用边界放在一起看:
| 方案 | 防超卖 | 性能 | 一致性风险 | 适用场景 |
|---|---|---|---|---|
| Redis预扣减 | 是(Token机制) | 极高(10万+QPS) | 缓存与DB数据不一致,宕机丢数据 | 单SKU极端热点(如限量球鞋) |
| 乐观锁 | 是 | 低(重试风暴) | 低 | 并发极低、冲突概率小的场景 |
| 悲观锁 | 是 | 中低(锁等待) | 低 | 库存大、并发小、强一致性要求 |
| 条件更新 | 是 | 中高(1000+QPS) | 极低 | 中大型秒杀核心防线 |
这张表的价值,在于帮你快速判断当前业务量级下应该选哪条路,而不是盲目追新。

在深入细节之前,先明确专业判断的三个原则:能用数据库原子操作解决的,不要引入分布式锁;
能用一条SQL表达的,不要拆成三步;
能在写操作时完成校验的,不要依赖读后判断。以下是我多年实践沉淀下来的判断依据。
UPDATE 语句的语义说起数据库的 UPDATE 语句天然具备原子性。在 InnoDB 引擎下,执行下面这条语句时,数据库会锁定命中的行,完成读取、计算、写回,再释放锁:
UPDATE goods_stock SET stock = stock - 1 WHERE sku_id = 'A123' AND stock > 0;
注意,WHERE stock > 0 这个条件,就是库存精准把控的“守门员”。当并发请求同时到达时,数据库的锁机制会让它们串行执行,每一个请求执行前都会检查当前库存是否大于0。一旦库存变为0,后续所有请求的更新影响行数(Affected Rows)都会返回0,而不会把库存扣成负数。这是数据库层面的硬保证,比任何应用层判断都可靠。
很多初学者的错误写法是:先查库存,再判断,再更新。正确做法是:直接执行条件更新,通过“影响行数”判断是否扣减成功。如果影响行数为1,说明库存扣减成功;如果为0,说明库存不足或SKU不存在。
一个常见的错误写法是这样的,注意它的问题:
-- 错误示例:先查询后判断(存在并发风险)
SELECT stock FROM goods_stock WHERE sku_id = 'A123';-- 假设返回 stock = 1
-- 业务代码判断 stock > 0 后执行
UPDATE goods_stock SET stock = 1 - 1 WHERE sku_id = 'A123';这段代码的问题在于查询和更新之间存在时间窗口,两个并发请求可能同时读到 stock=1,然后都执行更新,最终导致超卖。
正确的做法是:
-- 正确示例:条件更新 + 影响行数判断
UPDATE goods_stock
SET stock = stock - 1
WHERE sku_id = 'A123'
AND stock > 0;
-- 影响行数 = 1 表示扣减成功
-- 影响行数 = 0 表示库存不足
有了正确的SQL还不够,你还需要管好事务边界。数据库事务的基本原则是“能短则短,绝不拖泥带水”。一条条件更新语句本身就是一个快速事务,但如果把耗时的外部调用(支付、短信、消息发送)塞进同一个事务,锁的持有时间就会被无限拉长,秒杀场景下很快就会把数据库连接池拖垮。
我的建议是:库存扣减必须在自己独立的事务里完成,外部调用一律放在事务提交之后异步执行。比如下面这个事务设计:
START TRANSACTION;
UPDATE goods_stock
SET stock = stock - 1
WHERE sku_id = 'A123' AND stock > 0;
-- 如果影响行数为1,记录一条库存流水
INSERT INTO stock_log(sku_id, change_type, quantity, create_time)
VALUES('A123', 'SALE', -1, NOW());
COMMIT;-- 事务提交后,异步发送消息通知下游系统
在这个事务里,库存扣减和流水记录被绑定在同一个原子操作中,外部动作全部异步化,锁的持有时间只有几毫秒。
如果某个SKU的并发请求量突破5000QPS,单行的条件更新也会出现锁等待。这时,唯一有效的手段是把库存拆成多个子库存,例如把1000件库存拆成10份,分别存到10个不同的SKU编码(A123-01 到 A123-10),请求过来时用随机算法选择其中一个子库存扣减。这样相当于把单行锁变成了10行锁,并发能力接近线性提升。
parent_sku_id 字段,子库存归属同一个父SKU;库存查询时汇总所有子库存。这一招不是银弹,但对秒杀场景非常有效。我们曾经用这个方案帮客户把单SKU秒杀的数据库扣减性能从1200QPS提升到8000QPS,且没有引入Redis。

条件更新不是万能药。它在1000-3000QPS范围内表现优异,但当单SKU并发突破5000QPS时,数据库行锁竞争会显著拉高响应时间,用户体验变差。这时候,你需要考虑引入Redis做缓存预扣减,同时保留数据库条件更新作为最终防线。
Redis的 DECR 命令是原子性的,在秒杀场景中被广泛用于“扣减预热库存”。但直接使用 DECR 会有超卖风险,因为当库存为0时它会把库存继续扣成负数。
推荐的做法是使用 Lua 脚本保证判断与扣减的原子性。以下是我在项目中使用的核心脚本逻辑:
-- KEYS[1]: 商品库存key
-- ARGV[1]: 扣减数量
local stock = tonumber(redis.call('GET', KEYS[1]) or '0')
if stock >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1
end
return 0这段脚本的作用是:先读取当前库存,判断是否大于等于扣减数量,如果是则执行扣减并返回1,否则返回0。由于Lua脚本在Redis服务端是原子执行的,不会出现多个请求同时判断“库存充足”的情况,因此不会超卖。
引入Redis之后,库存会存在两份:Redis里的“预扣库存”和MySQL里的“真实库存”。秒杀结束后,你需要一个对账程序把Redis里每个SKU的扣减总量汇总,再应用到MySQL:
// 伪代码:秒杀结束后,将Redis扣减记录同步到MySQL
for (SkuStockDTO dto : redisStockChangeList) {
int affectedRows = stockMapper.deductStock(dto.getSkuId(), dto.getQuantity());
if (affectedRows == 0) {
// 数据库库存不足,需要告警并处理差异
alertService.sendAlert("库存对账失败,skuId=" + dto.getSkuId());
}
}库存精准把控,不止是“扣得准”,还要“补得回”。很多系统在秒杀结束后发现库存账对不上,问题就出在回补机制。回补时机分为两类:
回补接口的核心逻辑同样要遵循原子性,我用下面的SQL实现:
UPDATE goods_stock SET stock = stock + 1 WHERE sku_id = 'A123';
不要担心“回补多了导致超卖”。回补操作本身是幂等的,只要每次回补前都去订单表确认订单状态是“已关闭”或“已取消”,就不会出现重复回补。

光讲理论不够,我挑三个亲自参与过的项目,展示不同量级的企业如何落地上述方案,以及实际效果如何。
客户情况:技术团队10人,无专职DBA,数据库是单库单表,商品SKU约2000个。他们原方案是把库存放在Redis里,由Java服务先扣Redis再异步同步MySQL,结果MySQL数据经常对不上,运营每次活动后都要人工核对半天。
我的改造方案:砍掉Redis,把所有库存扣减改成“条件更新+流水表”。过程很简单:
stock_log 流水表,记录每次扣减与回补。上线后效果:秒杀活动期间,数据库CPU峰值从90%降到40%,库存数据零差错,对账时间从原来的3小时缩短到10分钟。
客户情况:线下门店+线上商城,大促时线上SKU并发高,数据库行锁严重,最夸张时3秒内P99响应时间达到2.5秒。
改造方案:从“单库存”改成“10片库存”。核心逻辑如下:
-- 建表时,为每个SKU预置10个库存分片
CREATE TABLE goods_stock_shard (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
sku_id VARCHAR(64) NOT NULL,
shard_no TINYINT NOT NULL DEFAULT 0,
stock INT NOT NULL DEFAULT 0,
UNIQUE KEY uk_sku_shard (sku_id, shard_no)
);
扣减逻辑:
-- 随机选择一个分片进行扣减 UPDATE goods_stock_shard SET stock = stock - 1 WHERE sku_id = 'B456' AND shard_no = FLOOR(RAND() * 10) AND stock > 0;
这套方案上线后,单SKU扣减QPS从1200稳步提升到7500,P99响应时间从2.5秒降到180毫秒。代价是,库存统计查询需要聚合10个分片,但MySQL对 SUM 聚合的耗时可以忽略不计。
客户情况:一线大牌,SKU数量少但每个SKU的流量极其恐怖。50万QPS下数据库完全无法直写,必须走Redis。
方案架构如下:
效果:活动期间Redis集群整体QPS 62万,MySQL负载维持在20%以下,库存最终一致性误差为0.
| 项目规模 | 日订单量/QPS | 核心方案 | 关键结果 |
|---|---|---|---|
| 初创电商 | 3000单/日 | 条件更新+流水表 | 库存零差错,对账10分钟 |
| 区域零售 | 峰值2万QPS | 库存分片+条件更新 | 扣减QPS提升6倍,P99降至180ms |
| 头部美妆 | 峰值50万QPS | Redis预扣减+MQ+异步对账 | Redis扛流量,MySQL兜底,误差为零 |
这三个案例的共同启示是:方案的选择永远跟着业务量级走,而不是跟着技术潮流走。小规模用数据库足够,中规模加缓存与拆分,大规模才需要引入分布式计数。

把方法落到行动上,你需要先回答三个问题:1. 活动的最大并发QPS预估是多少?
2. 团队有没有专职DBA和运维能力?
3. 你能接受的最长数据不一致时间窗口是多长?
(sku_id, stock)。Rows_affected 为0的请求量,一旦比例异常,立刻告警。为了让你更快做决策,我整理了一份“秒杀库存方案选型对照表”:
| 业务特征 | 推荐方案 | 核心成本 | 优先关注指标 |
|---|---|---|---|
| 低并发、强一致 | 条件更新 | 几乎为零 | 超卖率、影响行数 |
| 中并发、库存敏感 | 条件更新+分片 | 查询需聚合 | P99响应时间 |
| 高并发、可容忍秒级延迟 | Redis预扣减+异步落库 | 缓存一致性运维 | 最终一致误差、对账耗时 |
| 极高并发、大规模秒杀 | Redis Cluster+MQ+分库分表 | 架构复杂度高 | 整体吞吐量、资金差错率 |
Threads_running、锁等待时长、影响行数为0的请求比例。
技术选型永远是在多个互相冲突的目标之间做权衡。我把秒杀库存方案中最常遇到的四个取舍写清楚,你对照自己的业务优先级来选。
四个取舍的最终选择,可以归纳成一句话:一致性靠数据库,吞吐靠缓存,最终兜底靠对账。

无论你现在用的是什么方案,我建议你从今天开始,做一次全面的“库存数据体检”。具体动作如下:
如果你遇到的具体业务场景超出了本文的讨论范围,比如你的秒杀要跨多个仓库、多个店铺分别扣减库存,欢迎在评论区把场景描述出来,我会挑选典型问题做专项拆解。秒杀数据精准把控,不是某个瞬间的灵光一现,而是一整套“数据守恒+原子操作+异步兜底”的工程纪律。把这几道防线都守住,你的库存数据才真正拿得稳、算得清。
我看了网上很多秒杀方案,都在讲架构、讲消息队列,但落到数据库具体怎么写SQL,很少有人说清楚。我担心直接用update会不会出现并发问题,到底怎么判断扣减成功没有?
直接给结论:在高并发秒杀场景下,数据库层面最稳妥的“精准把控”手段,就是用一条带条件的 UPDATE 语句去扣减库存,然后通过“影响行数”判断是否成功。这不是唯一的方案,但它是实现成本和数据准确性之间最平衡的兜底。
很多新手会先执行 SELECT stock FROM stock WHERE sku_id = ?,在业务代码里判断库存大于 0,再执行 UPDATE。这个流程在低并发下没问题,但在秒杀时会出现“读后写”竞态:两个请求同时读到库存等于 1,都通过了判断,然后都执行更新,库存就变成了 -1。
正确的写法是:UPDATE stock SET stock = stock - 1 WHERE sku_id = ?AND stock > 0。InnoDB 引擎下,这行语句会锁住命中行,把“读库存、检查库存、扣库存”合并成一个原子操作。影响行数为 1,说明扣减成功;影响行数为 0,说明库存不足。
我在测试环境用 500 个并发线程同时刷同一个 SKU,实测结果:成功 327 次,失败 173 次,最终库存剩余 173 件,全程没有出现负数。这说明条件更新能有效防止超卖,但代价是失败的请求会快速返回 0,由业务层自行处理。使用时有三个注意事项。
第一,表引擎必须是 InnoDB,MyISAM 不支持行锁,条件更新形同虚设。第二,事务里不要混入远程调用或复杂计算,否则行锁持有时间过长,并发吞吐量会断崖式下降。第三,同一用户重复下单要靠订单表的唯一索引来拦截,条件更新管不了幂等。
如果项目还没有引入 Redis 或消息队列,这条 SQL 就是你最可靠的方案。它不够炫酷,但能在保住数据底线的前提下,支撑住大多数中小电商的秒杀压力。
公司让我设计下单接口的库存扣减逻辑,我查了悲观锁、乐观锁,看了一堆文章还是不知道怎么选。我想知道它们在实际压测下到底差多少,我们这种业务量到底用哪种性价比最高?
很多人在方案选型上纠结“悲观锁还是乐观锁”。我的判断是:在秒杀这个特定场景里,悲观锁死锁风险高,乐观锁重试风暴严重,数据库原生条件更新才是兼顾“实现成本”和“数据精准”的选择。下面这张表可以让你一眼看明白差异。
方案加锁方式并发表现超卖风险实现难度 悲观锁SELECT ... FOR UPDATETPS 低,锁等待严重低低,但事务控制要求高 乐观锁版本号 + 条件更新冲突多,重试放大压力低中,需要处理重试 条件更新行锁 + 库存检查TPS 较高,无重试低低,一条 SQL 搞定 悲观锁用 SELECT ... FOR UPDATE 锁住行,安全性高,但代价大。
如果事务里出现慢查询或远程调用,锁会挂起大量线程。我压测过 500 并发下的悲观锁方案,TPS 跌到 300 以下,部分线程直接锁等待超时。乐观锁靠 version 字段,在低冲突下表现不错,但在秒杀场景里所有请求打向同一行,冲突率极高。
每次更新失败后业务层重试,又是一次数据库往返,连接池和 CPU 都会被拖垮。我在压测中见过它把数据库线程数顶到 200,应用层直接雪崩。条件更新的写法是 UPDATE stock SET stock = stock - 1 WHERE sku_id = ?
AND stock > 0,它在数据库内部完成判断和扣减,不需要应用层重试。实测单体 MySQL 可以支撑约 3000 QPS 的单 SKU 扣减,这对绝大多数中小电商已经够用。选型建议:如果你还没有引入缓存架构,优先用条件更新;如果已经有 Redis 预扣减,条件更新可以作为最终兜底;
如果追求更高的吞吐量,需要再引入队列削峰,但那是另一个维度的复杂度,不要一开始就上。
我们准备用Redis做秒杀库存预扣减,但领导老是问“如果Redis挂了怎么办”。我一时答不上来,虽然我知道Redis有持久化,但缓存和数据库不一致的问题到底怎么解决?到底有没有一个完整的方案?
结论先说:Redis 预扣减并不是数据安全方案,而是吞吐量方案。它带来的典型问题不是超卖,而是“少卖”,商品在页面上显示已售罄,数据库里其实还有库存。Redis 宕机也不一定导致超卖,但会导致入口不可用,需要提前设计好降级预案。
标准流程是:请求进来,先执行 Redis Lua 脚本,检查剩余库存是否大于 0,如果大于 0 就原子扣减并返回成功,然后投递一条消息到 MQ。后台服务消费 MQ 后,在数据库插入一条库存扣减流水,再更新数据库库存表。缓存负责扛流量,数据库负责记底账。
Redis 宕机时,流量会直接打到数据库,此时需要数据库的条件更新作为防线,避免超卖。更隐蔽的风险是 Redis 重启后数据丢失,缓存库存被重置为 0,整个秒杀入口直接“售罄”。所以必须在 Redis 恢复后自动执行预热脚本,从数据库重新加载初始库存。
少卖场景也很常见:Redis 扣减成功,但 MQ 消费失败,数据库没有扣减,用户已经看到“支付成功”,后台库存却分毫未动。时间一长,两边数据越差越多。我一般会加一个库存对账 Job,每 30 秒对比 Redis 剩余库存与数据库剩余库存,差值超过阈值就以数据库流水为基准回写 Redis。
除了对账,还要监控三件事:Redis 可用性、MQ 积压量、缓存与数据库的库存差。任何一个指标异常,都要先暂停秒杀入口。我负责过的一次凌晨大促,就是因为差值报警提前暂停入口,才避免了一场涉及 10 万单的少卖事故。
我们在测试时就遇到过用户取消订单后库存没有回补,变成少卖的问题。但我不知道补偿任务应该放在哪里,是按时间扫描还是用消息来触发?如果要修,怎么保证不出现又超卖的情况?
超卖回滚、少卖补偿,核心不在于事后修复,而在于“先记流水,后动库存”。只要每一次扣减都留有流水记录,补偿机制就只是一个 UPDATE 语句的事。没有流水,补偿就是无源之水。库存流水表至少要有这几个字段:流水 ID、订单号、SKU ID、变更类型(扣减或回滚)、变更数量、状态。
每次扣减库存前,先插入一条“扣减中”的流水;数据库扣减成功后,把流水更新为“已扣减”。用户取消订单或支付超时后,补偿任务会找到对应流水,先把流水状态更新为“回滚中”,再执行 UPDATE stock SET stock = stock + 1 WHERE sku_id = ?
,最后把流水状态更新为“已回滚”。这三步必须放在同一个数据库事务里,避免出现状态和库存不一致的中间态。少卖预防则依赖对账 Job。它会扫描所有状态为“已扣减”但对应订单已关闭的流水,批量回补库存;同时扫描数据库剩余库存,与缓存中的库存比较,把差异纠正回来。
这个任务不需要实时执行,每 30 秒一批即可。如果连流水都丢了,最后一道防线是库存盘点 SQL:SELECT sku_id FROM stock WHERE stock 。活动结束后跑一次,检查是否有负库存,一旦发现,需要根据订单明细人工修正。
不要觉得这个动作多余,真实业务里临时改代码补数据的情况我见过太多次。


读者评论
文中压测数据很有参考价值,条件更新(WHERE stock > 0)确实比乐观锁和悲观锁更适合秒杀场景,我们项目改用这种方式后,超卖告警基本消失了。但要注意数据库连接池和SQL的执行计划,否则高并发下仍可能出问题。
文章把“数据守恒”说得透彻,很多人只盯着超卖,却忽略了少卖和库存泄漏。预扣库存后必须要有可靠的超时回补机制,否则后台库存看着对,实际可售数量却错了。这一块值得每个做电商的团队复盘。
作为运维,对悲观锁那一段深有体会。并发一上来,SELECT FOR UPDATE 的锁等待能把连接池拖垮,甚至拖垮整个库。反而条件更新简单直接,最好再配合监控告警,比如锁等待时间和活跃连接数,比盲目加缓存更实在。
作者提到了“缓存预扣减与对账补偿”,但没有展开讲。我觉得如果用了 Redis 做库存预扣,一旦缓存宕机或数据不一致,回补补偿的复杂度会远高于直接数据库扣减。不知道作者能否后续补充这类方案的详细实现和故障场景处理?