SELECT ... FOR UPDATE 加锁范围由执行计划、隔离级别和索引结构共同决定:唯一索引精确匹配时只加记录锁;范围查询、非唯一索引或无索引时会加间隙锁甚至表锁。

FOR UPDATE 加的到底是哪几行锁?
MySQL 的 SELECT ... FOR UPDATE 不是“看到哪行锁哪行”,而是由执行计划 + 隔离级别 + 索引结构共同决定的锁范围。在 REPEATABLE READ 下,InnoDB 默认使用 Next-Key Lock(记录锁 + 间隙锁),也就是说:
- 如果
WHERE条件命中唯一索引(如主键、唯一二级索引),且能精确匹配单条记录 → 只加Record Lock(锁住该行) - 如果条件是范围查询(
id > 10 AND id )、非唯一索引、或查不到数据(id = 999不存在)→ 会锁住索引区间(含间隙),可能意外锁住“本不该改”的相邻行
常见错误现象:
-
SELECT * FROM order WHERE user_id = 123 FOR UPDATE,但user_id没有索引 → 全表扫描,升级为表锁(等价于锁整张表) -
SELECT * FROM stock WHERE sku = 'A001' FOR UPDATE,而sku是普通索引(非唯一)→ 锁住所有sku = 'A001'的行 + 前后间隙,其他事务插入同 sku 新记录会被阻塞
实操建议:
- 执行前先用
EXPLAIN确认是否走了索引,避免隐式全表扫描 - 尽量让
FOR UPDATE的条件走唯一索引,缩小锁粒度 - 不要依赖“WHERE 条件看起来很精确”就认为只锁一行——得看执行计划和索引类型
FOR SHARE 和 FOR UPDATE 在死锁链里怎么互相咬住?
FOR SHARE(共享锁)和 FOR UPDATE(排他锁)不是“和平共处”的:它们之间互斥,且都与意向锁(IS/IX)联动。一个典型死锁场景是两个事务交叉申请不同资源上的锁:
事务 A:SELECT FROM account WHERE id = 100 FOR UPDATE; → 锁住 id=100SELECT FROM account WHERE id = 200 FOR SHARE; → 等待 id=200 的共享锁(但事务 B 已持有)
事务 B:SELECT FROM account WHERE id = 200 FOR UPDATE; → 锁住 id=200SELECT FROM account WHERE id = 100 FOR SHARE; → 等待 id=100 的共享锁(但事务 A 已持有排他锁)
结果:互相等待,触发 MySQL 自动检测并回滚其中一个事务。
容易踩的坑:
- 混用
FOR SHARE和FOR UPDATE时没统一访问顺序(比如一个按 id 升序,另一个按降序) - 在同一个事务里对多行加锁,但加锁顺序不固定(例如从缓存随机取 ID 列表后遍历)
- 忘了
FOR SHARE也会阻塞FOR UPDATE(很多人误以为“只读锁不碍事”)
实操建议:
- 同一业务逻辑中,对多行加锁务必按同一字段升序排序后再执行(如
ORDER BY id ASC) - 能用
FOR UPDATE统一解决的场景,别拆成FOR SHARE+ 后续UPDATE—— 多一次 round-trip 就多一次锁竞争窗口 - 开启
innodb_print_all_deadlocks = ON,把死锁日志落盘,别只靠应用层报错排查
为什么加了索引还是锁了大范围?间隙锁怎么关不掉?
间隙锁(Gap Lock)不是 bug,是 InnoDB 在 REPEATABLE READ 下防止幻读的必要机制。即使你加了索引、WHERE 精确匹配,只要隔离级别是 RR,且索引不是唯一索引,就仍可能触发间隙锁。
例如:CREATE TABLE t (a INT, b INT, INDEX idx_b(b));SELECT * FROM t WHERE b = 5 FOR UPDATE;
→ 若 b=5 对应多行(非唯一),InnoDB 会锁住所有 b=5 的记录,并在 b=5 前后间隙加锁(比如 b=4 和 b=6 之间的空档),阻止其他事务插入 b=5 的新行。
这不是配置能“关掉”的——除非:
- 改用
READ COMMITTED隔离级别(此时间隙锁禁用,但会引发幻读) - 把索引改成唯一索引(
UNIQUE INDEX),让 InnoDB 自动优化为 Record Lock - 显式用
SELECT ... LOCK IN SHARE MODE或FOR UPDATE配合SELECT ... INTO @var提前判断是否存在,再决定是否 INSERT,绕开范围不确定性
性能影响明显:
- 间隙锁会让并发 INSERT 变慢,尤其在高写入场景下(如订单号生成、秒杀库存扣减)
-
SHOW ENGINE INNODB STATUS中的TRANSACTIONS段会显示大量waiting for gap lock
死锁预防 checklist(贴在团队 Wiki 最好不过)
这不是“调参就能好”的问题,而是代码层必须约束的行为模式:
- 所有
FOR UPDATE/FOR SHARE查询必须走已存在的索引,禁止type: ALL或type: index(全索引扫描) - 多行更新前,统一用
ORDER BY pk ASC排序,pk 必须是主键或唯一键 - 同一事务内,禁止先
SELECT ... FOR SHARE再UPDATE,应直接SELECT ... FOR UPDATE - 应用层设置合理超时:
innodb_lock_wait_timeout(默认 50s)太长,建议设为 5–10s,配合重试逻辑 - 检查慢查询日志里带
FOR UPDATE的语句,确认其Rows_examined是否远大于Rows_sent(说明扫描多、锁得多)
最常被忽略的一点:FOR UPDATE 的锁持续到事务结束(COMMIT 或 ROLLBACK),不是语句执行完就释放。所以长事务 + 悲观锁 = 锁持有时间不可控,比锁本身更危险。










