mysql在rr隔离级别下update加锁范围更广,是因为必须用next-key lock堵住幻读漏洞:只要where条件走索引且能界定范围,即使未匹配任何记录,也会锁定对应索引间隙。

MySQL在RR隔离级别下UPDATE加锁范围更广,不是因为InnoDB“想多锁”,而是它必须用next-key lock堵住幻读漏洞——只要WHERE条件走索引、能界定范围,哪怕一行没更新,也会锁住索引间隙。
RR下UPDATE默认用next-key lock,不是可选项
RR隔离级别要求事务内多次查询结果一致,而幻读(新插入的行被后续读到)会破坏这点。InnoDB的解法是:对所有当前读操作(包括UPDATE)强制使用next-key lock,即“记录锁 + 间隙锁”的组合。
- 主键等值且记录存在 → 实际只加
record lock,但这是优化后的结果,底层仍按next-key逻辑走通 - 非唯一索引等值(如
UPDATE t SET s=1 WHERE status = 'pending')→ 必然加next-key lock,锁住该status值对应的所有索引位置及前后间隙 - 范围条件(
id > 100、BETWEEN、LIKE 'abc%')→ 锁整个扫描覆盖的索引区间,不管有没有数据 - 唯一索引等值但记录不存在 → 不加记录锁,但对相邻索引值之间的间隙加
gap lock,例如主键有1和5,WHERE id = 3就锁(1, 5)
没命中记录也加锁,是因为锁的是“扫描路径”而非“匹配结果”
InnoDB加锁依据是索引扫描过程,不是最终是否找到行。执行UPDATE t SET x=1 WHERE k = 999时,引擎会先定位k=999在B+树中的插入位置,发现它落在(500, 1000)之间,于是直接对这个间隙加gap lock。
- 验证方式:事务A执行该UPDATE并保持未提交;事务B执行
INSERT INTO t VALUES (700, 'x')会被阻塞,但INSERT INTO t VALUES (300, 'x')成功 - 日志里会出现
lock_mode X locks gap before rec,说明锁对象是间隙,不是记录 - 这种行为在
READ COMMITTED下不会发生——没命中就不锁,但业务要承担幻读风险
没走索引会让锁范围爆炸式扩大
当WHERE条件无法使用索引(比如隐式转换:user_id = '123'而字段是INT;或函数:SUBSTRING(phone, 1, 3) = '138'),InnoDB只能全表扫描聚簇索引,对每行都加X lock,RR下还会对所有扫描过的间隙加gap lock。
- 效果上接近“逻辑表锁”:其他事务无法插入、更新、甚至用
SELECT ... FOR UPDATE读该表任意行 -
EXPLAIN中看到type: ALL、key: NULL就是危险信号 - 加索引不能避免间隙锁,但能避免全表扫描导致的伪表锁——后者才是真正扼杀并发的瓶颈
非唯一索引UPDATE实际锁两处,死锁温床
用非唯一索引做条件时,InnoDB必须分两步加锁:先锁二级索引项,再回表锁对应的聚簇索引主键行。这两步非原子,中间有时间窗口。
- 事务A锁了
idx_status中status = 1的索引项,正准备锁主键id=100 → 事务B已锁id=100,正等idx_status中另一条status = 1的索引项 → 死锁 - RR下还额外给该status值所在间隙加
gap lock,进一步扩大冲突面 - 即使你确认只有一行匹配,只要索引不唯一,就逃不开这个双锁路径
真正难处理的不是“锁得多”,而是锁范围不直观:它由B+树结构、索引类型、隔离级别、WHERE写法共同决定,且间隙锁本身不可见于INFORMATION_SCHEMA.INNODB_TRX。排查时得盯紧performance_schema.data_locks和SHOW ENGINE INNODB STATUS\G里的lock_mode提示。











