单条带条件的update即可防超卖,无需事务;它依赖innodb行级更新原子性,where stock>=n确保扣减前校验库存,affected_rows=1才成功,version字段仅防覆盖写不防超卖。

单条带条件的 UPDATE 就能防止超卖,根本不需要事务包裹;加 version 字段反而在高并发下拖垮性能。
为什么 UPDATE ... WHERE stock >= 1 比 SELECT FOR UPDATE 更可靠
它不依赖锁、不阻塞其他请求,靠的是 MySQL 行级更新的原子性:InnoDB 在执行时会自动对匹配行加排他锁,且整个“判断 + 扣减”一步完成。只要 WHERE 条件包含库存校验,哪怕 1000 个请求同时命中同一行,也只有第一个满足 stock >= 1 的能成功,其余全部返回 affected_rows = 0。
常见错误是先 SELECT stock FROM goods WHERE id = 1,再在应用层判断,最后无条件 UPDATE —— 这中间存在毫秒级窗口,必然超卖。
- 正确写法:
UPDATE goods SET stock = stock - 1 WHERE id = 1 AND stock >= 1 - 执行后必须检查返回的
affected_rows:等于 1 才算扣减成功 - 无需
BEGIN TRANSACTION或COMMIT,单条UPDATE在 InnoDB 中天然具备原子性和行锁
version 字段不是防超卖的银弹,而是防覆盖写的补丁
乐观锁的 version 机制本质是 CAS(Compare-and-Swap),它解决的是“数据被别人改了你还覆盖”的问题,不是“库存被抢光你还扣”的问题。真正防超卖的,永远是 WHERE stock >= ? 这个条件本身。
用 version 的典型陷阱:
- 先
SELECT id, stock, version FROM goods WHERE id = 1,拿到stock=5, version=12 - 再
UPDATE goods SET stock = 4, version = 13 WHERE id = 1 AND version = 12 - 如果此时另一请求已把 stock 扣到 4 并升 version 到 13,你的 UPDATE 就失败 —— 但你并不知道是库存不足,还是只是 version 不匹配
- 重试逻辑必须重新查 stock 和 version,高并发下极易陷入“查→算→失败→再查”循环,连接和 CPU 压力陡增
事务隔离级别选 READ COMMITTED 就够用
很多人迷信 REPEATABLE READ 更安全,其实它在非唯一索引上容易触发间隙锁,把本不该锁的记录也锁住,反而降低并发度。而 READ COMMITTED 每次 SELECT 都读最新已提交值,配合原子 UPDATE 完全够用。
真正危险的是混用:
- 在同一个事务里,既用
SELECT ... FOR UPDATE加锁,又穿插普通SELECT或UPDATE—— 锁范围模糊,极易死锁 - 日志里出现
Deadlock found when trying to get lock,八成是这种写法惹的祸 - 如果你真要用
FOR UPDATE,请确保整段逻辑极短、无外部调用、无长耗时计算
最常被忽略的一点:UPDATE 的 WHERE 条件必须精确匹配业务语义。比如扣减 3 件,不能只写 stock >= 1,而要写 stock >= 3;否则可能扣到负数还返回成功。这个细节,比锁类型选择更关键。











