真正起作用的是“原子性判断+更新”,如UPDATE goods SET stock=stock-1 WHERE id=1 AND stock>=1,执行后检查affected_rows;SELECT FOR UPDATE易引发锁竞争与死锁,非必要不推荐。

单靠事务 + SELECT FOR UPDATE 不能可靠防止超卖,反而容易引发锁竞争雪崩。 真正起作用的是“原子性判断+更新”本身,而事务只是辅助手段。下面说清楚怎么做、为什么、以及最容易栽跟头的地方。
UPDATE WHERE 条件中必须包含库存校验
这是最简单也最有效的防线。数据库执行 UPDATE 时,只有满足 WHERE 中所有条件才会真正修改数据,且整个操作是原子的。
- 错误写法(先查后更新):
SELECT stock FROM goods WHERE id=1→ 应用层判断stock > 0→UPDATE goods SET stock=stock-1 WHERE id=1 - 正确写法(一步到位):
UPDATE goods SET stock=stock-1 WHERE id=1 AND stock >= 1 - 执行后检查
affected_rows:等于 1 表示扣减成功;等于 0 表示库存不足或已被抢光 - 无需显式
START TRANSACTION—— 单条UPDATE在 InnoDB 中天然具备原子性和行锁保障
为什么 SELECT FOR UPDATE 很危险
它确实加了行锁,但代价是把并发请求强行串行化。在秒杀场景下,这等于主动制造瓶颈。
- 所有请求都卡在
SELECT ... FOR UPDATE这一行,排队等锁,TPS 断崖下跌 - 一旦某个事务慢(比如日志刷盘延迟、网络抖动),后面所有请求都会被拖住
- 容易触发死锁:两个事务按不同顺序访问多行时,
FOR UPDATE极易形成循环等待 - 错误日志里常见
Deadlock found when trying to get lock就是它惹的祸
乐观锁 version 字段不是银弹
它能避免覆盖写,但不解决“查-判-更”中间的窗口期问题,且重试逻辑必须由应用层兜底。
- 必须配合应用层重试:第一次
UPDATE ... WHERE id=? AND version=?失败后,要重新SELECT当前stock和version,再试一次 - 高并发下重试失败率陡增:1000 人抢 10 件库存,平均每人要重试几十次,CPU 和连接压力翻倍
-
version字段本身会增加写放大,对热点行更新频率越高,undo log 和 buffer pool 压力越大 - 注意:不能只靠
version判断库存是否充足,WHERE中仍需保留stock > 0条件
真正要盯住的三个细节
很多团队踩坑,不是因为方案选错,而是细节没控住。
-
stock字段必须是INT或BIGINT,禁止用FLOAT或DECIMAL—— 浮点计算误差会导致扣成负数 - 索引必须落在查询条件列上:如果用
WHERE product_id=123 AND stock>=1,就要建联合索引INDEX idx_pid_stock (product_id, stock),否则全表扫描+锁表 - 应用层拿到
affected_rows == 0后,必须立刻返回“库存不足”,不能静默重试或 fallback 到其他逻辑 —— 否则可能触发重复下单或状态错乱











