MySQL在RR隔离级别下,更新不存在的记录会加间隙锁以防止幻读,这是刚性设计而非bug;只要查询走索引、条件可界定范围且记录不存在,InnoDB即锁定对应索引间隙,如id=5不存在时锁定(1,10)区间,导致INSERT id=5被阻塞。

MySQL 更新不存在的记录会加间隙锁,不是 bug,是 RR 隔离级别下防止幻读的刚性设计。只要查询条件落在索引区间内、且对应记录实际不存在,InnoDB 就会锁住那个“空档”——哪怕你 UPDATE 或 SELECT ... FOR UPDATE 什么都没查到。
为什么 WHERE id = 5 没查到数据,INSERT id = 5 还会被阻塞
关键在“索引结构”和“锁粒度”。假设主键 id 是唯一索引,当前只有 (1, 'a') 和 (10, 'b') 两行:
-
UPDATE t SET name='x' WHERE id = 5—— 看似等值,但因为id = 5不存在,InnoDB 不会只“不加锁”,而是按 next-key 锁规则,定位到索引中(1, 10)这个间隙,并对整个(1, 10)加 gap lock - 此时另一个事务执行
INSERT INTO t VALUES (5, 'c'),就会被挡在间隙外,一直等待 - 注意:这个行为只在
REPEATABLE READ下默认生效;READ COMMITTED下 gap lock 被禁用(但需配合binlog_format=ROW)
哪些查询一定会触发间隙锁(即使没命中数据)
不是所有“没查到”都会加间隙锁,触发前提是:查询走索引 + 条件可界定范围 + RR 隔离级别。典型场景包括:
- 唯一索引上的**等值查询但记录不存在**:如
SELECT * FROM t WHERE id = 7 FOR UPDATE,而表中无id = 7 - 任何索引上的**范围查询**:如
UPDATE t SET status=1 WHERE create_time > '2024-01-01',哪怕当前没一条满足该时间的数据 - 非唯一索引上的**等值查询**:如
SELECT * FROM t WHERE status = 'pending' FOR UPDATE,即使只有一条匹配,也会锁住前后间隙 - LIKE 前缀匹配(有索引支持):如
WHERE name LIKE 'abc%',本质是范围扫描,同样触发 gap lock
怎么验证当前语句到底加了什么锁
别猜,直接查 INFORMATION_SCHEMA.INNODB_TRX 和 INFORMATION_SCHEMA.INNODB_LOCKS(MySQL 5.7+)或使用 SHOW ENGINE INNODB STATUS\G。重点关注:
- 事务的
TRX_STATE是否为LOCK WAIT - 阻塞方的
LOCK_TRX_ID和被阻塞方的LOCK_TRX_ID是否形成闭环 - 锁类型字段(如
lock_mode显示GAP、NEXT-KEY或RECORD) - 锁住的索引名和
lock_data值——例如显示lock_data: 5未必是锁了记录,也可能是(1,5]的右边界
绕过间隙锁的实操路径
核心不是“禁用”,而是“让 MySQL 认定你只需要锁记录”。可行做法:
- 强制走唯一索引 + 等值 + 数据存在:先
SELECT id FROM t WHERE ... LIMIT 1拿到主键,再SELECT ... WHERE id = ? FOR UPDATE - 改隔离级别:临时切到
READ COMMITTED(注意 binlog 格式和主从一致性) - 用
SELECT ... FOR UPDATE NOWAIT替代默认阻塞,让失败立刻暴露,避免长等待掩盖问题 - 业务层预占位:插入前先尝试
INSERT ... ON DUPLICATE KEY UPDATE,利用唯一约束失败快速反馈,而非依赖锁等待
真正难处理的从来不是“怎么加锁”,而是“你以为没加锁,其实已经锁了一大片”。间隙锁不显式报错,也不出现在 SELECT 结果里,但它就在那里,安静地拦住所有想往空隙里钻的 INSERT。


















