<p>最直接有效的解法是单条带条件的UPDATE语句:UPDATE goods SET stock = stock - 1 WHERE id = 123 AND stock >= 1,它依赖InnoDB行级更新原子性,全程不可中断,执行后必须检查affected_rows是否为1。</p>

UPDATE WHERE stock >= 1 是最直接有效的解法
别绕弯子——真正能防超卖的,就是一条带条件的 UPDATE 语句:UPDATE goods SET stock = stock - 1 WHERE id = 123 AND stock >= 1。它不依赖事务隔离级别、不靠应用层判断、不拼锁持有时间,靠的是 InnoDB 行级更新本身的原子性:定位行 → 加 X 锁 → 读值 → 判断 WHERE → 执行 SET → 释放锁,全程不可中断。
常见错误是漏掉 stock >= 1 条件,只写 WHERE id = 123。这样库存为 0 时仍会执行减法,变成负数,且 ROW_COUNT() 返回 1,应用层误判成功。
-
WHERE必须同时包含主键(或唯一索引)和库存判断,否则可能锁不住行,甚至升级为表锁 - 执行后必须检查
affected_rows:等于 1 才算扣减成功;等于 0 表示库存不足或记录不存在 - 绝不能在
UPDATE后再SELECT一次库存做二次校验——这又把原子操作拆回了竞态老路
SELECT FOR UPDATE + UPDATE 两步法容易踩哪些坑
很多人以为显式加锁就安全了,结果线上还是超卖,问题往往出在锁没真正生效或事务控制失当。
SELECT ... FOR UPDATE 只在事务内有效,且锁从执行那一刻起就持有,直到 COMMIT 或 ROLLBACK 才释放。中间任何延迟(比如日志打印、HTTP 调用、异常处理)都会拖长锁持有时间,引发排队、超时甚至死锁。
- 必须关闭
autocommit=1,否则每条语句自成事务,FOR UPDATE查完立刻释放锁,后续UPDATE已无锁保护 - 多个事务若按不同顺序加锁(如事务 A 先锁商品 1 再锁商品 2,事务 B 反之),极易触发死锁,
SHOW ENGINE INNODB STATUS里能看到lock wait timeout exceeded - 查询条件没走索引(如
WHERE status = 1但status无索引),InnoDB 会升级为表锁或锁全表,性能断崖式下跌
为什么 SERIALIZABLE 隔离级别不能直接防超卖
SERIALIZABLE 是最高隔离级别,但它只对“同一事务内的读写”起作用。如果业务代码把查库存和扣库存拆成两个独立请求(比如两次 HTTP 调用),或者用了框架默认的自动提交模式,那 SERIALIZABLE 就完全失效。
MySQL 9.6.0是面向Linux平台的2026年创新版本,核心架构迎来重大革新。其将外键约束与级联操作从InnoDB引擎层上移至SQL层,确保所有数据变更均被完整记录至Binlog,彻底解决了CDC(变更数据捕获)与主从复制中的数据不一致难题。此外,该版本引入container_aware启动选项以原生适配容器环境,并对审计日志进行了组件化重构,为追求极致数据一致性与云原生体验的开发者提供了全新选择。
它的机制是:普通 SELECT 自动转成 SELECT LOCK IN SHARE MODE,写操作加排他锁——但前提是所有操作都在一个显式开启的事务里完成。一旦事务提前结束(比如 COMMIT 过早),锁就释放,下一个请求立刻读到旧值。
- 必须显式执行
BEGIN或START TRANSACTION -
SELECT必须带FOR UPDATE(不能只靠隔离级别隐式加锁) - 整个逻辑必须在同一个事务中走完,中途不能
COMMIT或隐式提交
字段约束和类型不是可选项,而是兜底防线
即使你写对了 UPDATE ... WHERE stock >= 1,也架不住有人绕过应用直连数据库执行 UPDATE goods SET stock = -100,或者 SQL 注入篡改语句。这时候 UNSIGNED 和 CHECK 约束就是最后一道闸。
UNSIGNED INT 能防止负数插入,但注意:它不能阻止 stock = 0 时执行 stock = stock - 1 导致溢出变大数(4294967295)。所以 WHERE stock >= 1 仍是核心。
- MySQL 8.0.16+ 支持
CHECK (stock >= 0),任何违反该约束的INSERT/UPDATE会直接报错Check constraint 'goods_chk_1' is violated -
CHECK在 SQL 层实时校验,不增加运行时开销,比应用层判断更可靠 - 不要指望约束替代原子 SQL——它是兜底,不是主力
真实场景里,最容易被忽略的不是语法,而是「锁是否真的加在了目标行上」和「事务是否真的跨过了整个业务逻辑」。哪怕 SQL 写得再漂亮,只要索引缺失、autocommit 开着、或者 COMMIT 写早了半行,超卖就会发生。








