for update必须在事务中执行,否则锁立即释放;它加排他锁(x锁),阻塞其他事务的for update、for share及dml操作,且查无结果时仍会加间隙锁。

FOR UPDATE 必须在事务中执行,否则锁无效
很多人写完 SELECT * FROM accounts WHERE id = 1 FOR UPDATE 就直接去更新,结果发现并发时还是出错——根本原因是没包在事务里。FOR UPDATE 的锁只在当前事务生命周期内有效,语句执行完、事务未提交,锁就释放了。
正确姿势是:
- 显式开启事务:
BEGIN或START TRANSACTION - 紧接着执行带
FOR UPDATE的SELECT - 再做业务判断和
UPDATE - 最后
COMMIT或ROLLBACK
漏掉任意一步,比如忘记 COMMIT,会导致锁长期持有,阻塞其他事务;更常见的是压根没 BEGIN,锁秒释,形同虚设。
FOR SHARE 和 FOR UPDATE 的锁冲突规则很明确
FOR SHARE 加的是共享锁(S锁),FOR UPDATE 加的是排他锁(X锁)。它们之间不是“能读不能写”这种模糊描述,而是有确定的互斥关系:
- 事务A执行
SELECT ... FOR SHARE→ 其他事务可以再FOR SHARE,但不能FOR UPDATE或UPDATE - 事务A执行
SELECT ... FOR UPDATE→ 其他事务连FOR SHARE都会被阻塞,直到A提交 - 两个事务同时对同一行
FOR UPDATE→ 后者会等待,超时抛Lock wait timeout exceeded
注意:这些互斥只发生在「同一行命中索引」的前提下。如果 WHERE 条件没走索引,InnoDB 会升级为表锁,所有行都受影响。
查不到数据时,FOR UPDATE 仍会加间隙锁
这是最容易被忽略的坑。例如表 user 主键是 id,当前只有 id=1,2,5 三条记录,执行:
BEGIN; SELECT * FROM user WHERE id = 4 FOR UPDATE;
表面上没查到任何行,但 InnoDB 会在索引间隙 (2,5) 上加间隙锁(Gap Lock),阻止其他事务插入 id=3 或 id=4 的记录。这在防止幻读的同时,也容易引发意外阻塞。
如果你的业务逻辑依赖“查无结果就插入”,又用了 FOR UPDATE,必须意识到这个间隙锁的存在。解决办法包括:
- 改用
INSERT ... ON DUPLICATE KEY UPDATE替代先查后插 - 确认查询条件能精确命中索引,避免范围扫描触发间隙锁
- 必要时用
SELECT ... FOR UPDATE SKIP LOCKED跳过已被锁的行(适用于队列类场景)
FOR SHARE 支持 NOWAIT 和 SKIP LOCKED,FOR UPDATE 也一样
MySQL 8.0 给这两个语句都加了 NOWAIT 和 SKIP LOCKED 选项,大幅降低死锁和等待风险。
NOWAIT 让语句不等待锁,立刻报错:
SELECT * FROM orders WHERE status = 'pending' FOR UPDATE NOWAIT; -- 如果行已被锁,直接返回 ERROR 1205 (HY000): Deadlock found when trying to get lock
SKIP LOCKED 则跳过当前被锁的行,继续找下一个可用行,常用于任务分发:
SELECT * FROM tasks WHERE worker_id IS NULL ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
注意:NOWAIT 和 SKIP LOCKED 是 MySQL 8.0+ 特性,低版本不支持;且它们不能同时使用。
实际部署前务必验证存储引擎是 InnoDB,隔离级别是 REPEATABLE READ 或 READ COMMITTED——READ UNCOMMITTED 下 FOR UPDATE 行为异常,SERIALIZABLE 则默认给所有读加锁,反而削弱控制粒度。











