update带条件扣减最可靠,因其原子性天然防竞态;仅当业务需复杂校验(如跨商品组合、预留编号)时才需select...for update+显式事务。

直接用 UPDATE 带条件扣减,比“先查再更新”更可靠——它把判断和修改压进原子操作,天然避开 check-then-act 竞态。只有当业务必须做复杂校验(比如跨商品组合、库存预留、价格联动)时,才需要升级到 SELECT ... FOR UPDATE + 显式事务。
UPDATE WHERE 条件扣减为什么能防超卖
核心在于 InnoDB 对 UPDATE 语句自动加行级排他锁,并在执行时原子性校验 WHERE 条件。库存不足时整条语句不生效,返回影响行为 0,无需额外查一次。
-
UPDATE inventory SET quantity = quantity - 1 WHERE id = 123 AND quantity >= 1:命中主键,只锁单行;quantity >= 1在引擎层完成判断,不是应用层 if 逻辑 - 必须确保
quantity是UNSIGNED INT,否则quantity - 1变负数不会报错,而是静默溢出为极大正数 - 该语句在
REPEATABLE READ下也安全——它不依赖快照读,也不需要显式事务包裹,本身就是一个隔离的写操作 - 失败时仅产生少量 undo log,比
FOR UPDATE+ 多语句事务对 purge 线程压力小
什么时候必须用 SELECT ... FOR UPDATE
当库存判断逻辑无法塞进单条 UPDATE 的 WHERE 条件里,比如要查多个字段、做计算、校验状态位、或需在扣减前生成唯一单号/预留编号时,就得上当前读 + 显式事务。
- 必须显式以
BEGIN或START TRANSACTION开启,否则SELECT ... FOR UPDATE不加锁——这是线上最常踩的坑 -
WHERE条件必须命中索引(最好是主键或唯一索引),否则可能升级为间隙锁甚至表锁,拖垮并发 - 事务内禁止混用快照读(普通
SELECT)做判断依据,它看到的仍是事务开启时的旧快照 - 锁等待超时默认是 50 秒(
innodb_lock_wait_timeout),高并发下容易耗尽连接池,建议设为 3–5 秒并配合指数退避重试
事务隔离级别怎么选
MySQL 默认的 REPEATABLE READ 足够应对大多数订单场景,但要注意它不解决幻读——而幻读正是“查库存 > 0 → 扣库存”逻辑崩坏的根源。
- 用
SELECT ... FOR UPDATE就是在REPEATABLE READ下主动绕过 MVCC 快照,强制走当前读,同时加 Next-Key 锁(行锁 + 间隙锁) -
READ COMMITTED下每次SELECT都读最新已提交数据,但FOR UPDATE锁范围更小(只锁匹配行,不锁间隙),适合读多写少且对间隙插入无强约束的场景 - 别轻易切到
SERIALIZABLE——它会让所有SELECT都隐式加锁,吞吐量断崖下跌,秒杀系统反而更容易雪崩
真正难的不是写对那几行 SQL,而是确认 WHERE 条件是否真的走索引、事务边界是否被 ORM 自动提交截断、以及锁等待失败后有没有合理降级路径——这些细节一漏,就又回到 -23 件库存的老路上。











