
UPDATE 加 WHERE 条件必须检查库存是否足够
超卖本质是多个请求同时读到“还有 1 件”,然后都执行减库存,最终变成 -1。靠事务本身不能拦住这个,START TRANSACTION 只保证语句执行的原子性,不保证“读-判-写”这一整段逻辑的原子性。
正确做法是在 UPDATE 里把库存判断塞进 WHERE 子句,让数据库在更新前原子地校验:
UPDATE products SET stock = stock - 1 WHERE id = 123 AND stock >= 1;
执行后检查 ROW_COUNT()(MySQL 返回影响行数):
- 返回 1 → 更新成功,库存够
- 返回 0 → 没人更新成功,说明库存已不足或记录不存在
别用 SELECT + UPDATE 组合做库存扣减
这是最常见也最危险的写法。哪怕加了 SELECT ... FOR UPDATE,在应用层做判断再发 UPDATE,中间仍存在时间窗口——比如锁被释放后、UPDATE 发出前,另一个事务可能已抢先完成扣减。
典型错误链路:
SELECT stock FROM products WHERE id = 123 FOR UPDATE;<br>→ 应用判断 stock > 0<br>→ sleep(0.1) // 模拟网络延迟或处理耗时<br>→ UPDATE products SET stock = stock - 1 WHERE id = 123;
此时第二个请求可能已在 SELECT 后、UPDATE 前完成整个流程,第一个请求的 UPDATE 就会把库存扣成负数。
乐观锁适合低冲突场景,高并发下慎用 version 字段
用 version 字段做更新校验(如 UPDATE ... SET stock = ?, version = ? WHERE id = ? AND version = ?)看似安全,但高并发下失败重试成本极高——大量请求会因版本不匹配而反复查询、计算、重试,加剧数据库压力和响应延迟。
适用前提很明确:
- 冲突概率低(比如用户下单频次远低于库存总量)
- 业务能接受一定比例的失败并友好提示(如“库存变动,请重新确认”)
- 重试逻辑有兜底(不能无限循环重试)
如果单商品日销量过万、秒杀类场景,乐观锁会明显拖慢整体吞吐,不如直接用带条件的 UPDATE + 业务层快速失败。
事务隔离级别选 READ COMMITTED 就够用
很多人以为必须上 SERIALIZABLE 或 REPEATABLE READ 才安全,其实没必要。MySQL 默认的 REPEATABLE READ 在非唯一索引场景下可能引发间隙锁,导致无关记录被锁住,反而降低并发度。
READ COMMITTED 更轻量,且配合 WHERE 中的库存判断,完全能防止超卖:
- 不会读到未提交的脏数据
- 每次
SELECT(如果真要用)都读最新已提交值 - 关键还是那条带条件的
UPDATE,它不依赖隔离级别,只依赖行级锁 + 条件判断
真正要注意的是:不要在事务里混用 SELECT ... FOR UPDATE 和普通 SELECT,容易因锁范围不清引发死锁或长等待。
高并发防超卖不是靠“加更重的锁”或“升更高隔离级别”,而是把判断和更新压进一条原子 SQL。最容易被忽略的,是开发时本地单线程测试永远看不出问题,一上生产流量,WHERE 条件漏写、ROW_COUNT() 不检查、或者用 ORM 自动生成的 update 忽略了库存约束——这些才是线上超卖的真正源头。


















