select for update 必须在显式事务中才生效,单独执行不加锁;正确用法是begin后执行for update触发当前读,再业务判断和update,最后commit;非索引字段可能升级为间隙锁或表锁;快照读不可用于库存判断;原子性update更安全高效;repeatable-read下需for update避免幻读导致超卖;锁等待超时易耗尽连接池,应设合理超时并退避重试。

SELECT FOR UPDATE 必须在事务内才生效
单独执行 SELECT ... FOR UPDATE 不会加锁,也不会阻塞其他事务——它只在显式事务块中起作用。很多人写完 SELECT ... FOR UPDATE 就直接 commit,却忘了前面没写 BEGIN 或 START TRANSACTION,结果锁根本没建起来,超卖照旧。
正确姿势是:必须用 BEGIN 显式开启事务,执行 SELECT ... FOR UPDATE(触发当前读),再做业务判断和 UPDATE,最后 COMMIT 或 ROLLBACK。
- MySQL 8.0+ 支持
SET autocommit = 0全局关自动提交,但不推荐——容易漏COMMIT导致长事务锁表 -
FOR UPDATE在非主键/非唯一索引字段上查询时,可能升级为间隙锁甚至表锁,务必确认 WHERE 条件命中索引 - 如果事务里混用了快照读(普通 SELECT)和当前读(
FOR UPDATE),快照读看到的仍是旧版本,别拿它做库存判断依据
UPDATE 带条件比先查后更安全
哪怕不用 FOR UPDATE,直接用原子性 UPDATE 也能拦住大部分超卖。比如:UPDATE inventory SET quantity = quantity - 1 WHERE id = 123 AND quantity >= 1。这条语句返回影响行数,如果为 0,说明库存不足,无需额外查一次。
它比“先 SELECT 再 UPDATE”少一次网络往返,也避免了两次操作之间的时间窗口被其他事务插队。
- 注意
quantity字段必须是UNSIGNED类型,否则扣成负数不会报错,只是静默更新 - 如果要扣减多个商品或跨表操作,单条
UPDATE无法覆盖,这时才需要FOR UPDATE+ 事务协调 - 该写法在低并发下足够,但高并发时大量失败的
UPDATE会产生很多 undo log,压力集中在 InnoDB 的 purge 线程上
REPEATABLE-READ 下幻读会让库存校验失效
默认隔离级别 REPEATABLE-READ 对普通 SELECT 是快照读,事务内多次查库存都返回同一个值——哪怕别的事务已经把库存扣光了。这就是为什么“查库存 > 0 → 扣库存”逻辑在并发下必然超卖。
FOR UPDATE 强制走当前读,绕过 MVCC 快照,读到的是最新已提交数据,同时加 Next-Key 锁(行锁 + 间隙锁),防止其他事务插入新记录或修改该行。
- 幻读不是“读到了不存在的行”,而是“读不到刚发生的变更”,
FOR UPDATE正是为堵这个洞 - 如果业务允许少量超卖(如优惠券发放),可降级到
READ-COMMITTED,但得同步改代码——此时FOR UPDATE只锁行,不锁间隙 - 别指望
SELECT ... LOCK IN SHARE MODE防超卖,它只防写不防 insert,对库存场景无效
锁等待超时和连接耗尽比超卖更致命
FOR UPDATE 在高并发下会排队等锁,一旦锁等待时间超过 innodb_lock_wait_timeout(默认 50 秒),事务直接报 ERROR 1205 (HY000): Deadlock found 或超时中断。更危险的是,每个等待中的连接都占着一个数据库连接,连接池很快被吃光,后续请求全卡在连接获取阶段。
- 务必设置合理的
innodb_lock_wait_timeout(建议 5–10 秒),并在应用层捕获Lock wait timeout exceeded错误,做退避重试 - 避免在事务里做 RPC、发邮件、调外部 API——锁持有时间越长,阻塞面越大
- 热点商品(如秒杀 ID=1 的 iPhone)天然形成单点锁竞争,需提前拆分库存粒度(比如按仓库、批次、虚拟仓 ID 分片),而不是死守一个主键
真实线上环境里,锁不是万能解药,它把问题从“数据错”转移到“响应慢”和“连接崩”。选方案前得想清楚:你到底怕超卖,还是怕用户等 3 秒才下单成功。











