UPDATE未命中行仍加锁,是因为InnoDB在RR隔离级别下对索引扫描路径加间隙锁(Gap Lock),如WHERE id=999无数据时,会锁定(998,1000)等相邻索引值之间的空隙,阻止INSERT插入该值。

为什么UPDATE WHERE id = 999没找到行,INSERT id = 999却被卡住
因为InnoDB加锁对象不是“数据行”,而是“索引扫描路径”。哪怕WHERE id = 999查不到任何记录,只要id是主键或唯一索引,InnoDB仍会定位到该值在B+树中应插入的位置,并对前后两个真实存在的索引值之间的空隙加Gap Lock。比如当前有id = 998和id = 1000两行,id = 999的间隙就是(998, 1000)——这个开区间被锁住,INSERT INTO t (id) VALUES (999)自然阻塞。
哪些等值查询一定会触发间隙锁(即使没数据)
触发间隙锁的关键条件有三个:RR隔离级别 + 走索引 + 条件可界定范围。以下场景必加Gap Lock:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
-
WHERE用主键或唯一索引做等值查询,但值不存在(如UPDATE users SET status=1 WHERE user_id = 12345,而表中最大user_id是12344)→ 锁(12344, +∞) -
WHERE用普通索引(如status)做等值查询,无论是否存在匹配行 → 锁该status值对应索引位置前后的间隙 -
WHERE用联合索引最左前缀做等值查询(如(type, created_at),查type = 'order')→ 锁所有type = 'order'可能落点的间隙
怎么确认是不是间隙锁在作怪
不能只看ROW_COUNT()返回0就认为“没锁”。验证必须靠并发行为+系统表交叉判断:
- 事务A执行
UPDATE t SET x=1 WHERE id = 500(假设无此id)并保持未提交 - 事务B执行
INSERT INTO t (id, x) VALUES (500, 'y')→ 若被阻塞,基本可断定是间隙锁生效 - 查
performance_schema.data_locks,过滤OBJECT_NAME = 't',若看到LOCK_MODE = 'X,GAP'且LOCK_DATA显示类似499, 501的边界值,即为实锤 -
SHOW ENGINE INNODB STATUS\G中搜lock_mode X locks gap before rec,这条提示只出现在间隙锁场景,且不会出现在INFORMATION_SCHEMA.INNODB_TRX里
READ COMMITTED能绕过,但别乱切
RC隔离级别下,UPDATE未命中行确实不加间隙锁,只加记录锁(没记录可锁,所以实际不锁)。但这不是银弹:
- 必须确保
binlog_format = ROW,否则主从复制可能出错 - RC无法防止幻读,业务逻辑若依赖“两次SELECT结果一致”,切换后可能出问题
- 仅对等值未命中有效;范围查询(如
WHERE id > 500)在RC下仍可能锁住命中的行,只是不锁间隙 - 全局改隔离级别影响大,更稳妥的做法是:先
SELECT id FROM t WHERE id = ?确认存在再UPDATE,或用INSERT ... ON DUPLICATE KEY UPDATE替代

















