间隙锁是MySQL在REPEATABLE READ级别下为防止幻读而锁定索引间隙的范围锁,只对当前读操作、范围条件或非唯一索引等值查询生效,不锁记录本身而锁(10,20)类空隙,INSERT ON DUPLICATE KEY UPDATE等场景易引发隐式间隙锁冲突。

间隙锁本质是“防插队”的范围锁
MySQL 的间隙锁(Gap Lock)不是锁某条记录,而是锁住索引中两个值之间的“空隙”,比如 id 为 10 和 20 之间没有数据,但 WHERE id BETWEEN 10 AND 20 FOR UPDATE 仍会锁住 (10, 20) 这个开区间——新插入 id=15 的行会被阻塞。它只在 REPEATABLE READ 隔离级别下生效,READ COMMITTED 下直接禁用间隙锁,也就默认接受幻读。
什么时候真正触发间隙锁?看查询条件和索引
间隙锁不会凭空出现,必须同时满足:当前是 REPEATABLE READ 隔离级别 + 当前读操作(如 SELECT ... FOR UPDATE、UPDATE、DELETE)+ 查询走的是**范围条件或非唯一索引等值查询**。
- 主键等值查询(如
WHERE id = 100)→ 只加记录锁(Record Lock),不加间隙锁 - 主键范围查询(如
WHERE id > 100)→ 加 Next-Key Lock(记录锁 + 间隙锁),覆盖 (100, +∞) - 非唯一索引等值查询(如
WHERE status = 'pending',且status有索引但不唯一)→ 锁住所有匹配值对应的索引项,以及它们之间的所有间隙 - 没走索引的查询 → 降级为表锁(严重!)
为什么 SELECT COUNT(*) WHERE holder_id = ? FOR UPDATE 能锁住间隙?
很多人以为只有 SELECT * 才能触发间隙锁,其实只要语句是当前读、且优化器用了索引(哪怕只查 COUNT(*)),InnoDB 就照样按规则加 Next-Key Lock。例如 holder_id 有索引,SELECT COUNT(*) FROM tree_nodes WHERE holder_id = 123 FOR UPDATE 会锁定所有 holder_id = 123 记录的索引位置,以及前后间隙——这意味着其他事务无法插入 holder_id = 123 的新节点,有效防止幻读,又避免拉回全量数据。
容易踩的坑:看不见的锁冲突和死锁风险
间隙锁的“不可见性”是最大陷阱:它不锁实际数据,所以 SHOW ENGINE INNODB STATUS 里看到的锁信息可能只显示“waiting for table metadata lock”,而真实原因是两个事务在不同范围上持有了重叠的间隙锁。
- 多个范围查询交叉时(如事务 A 锁 (10, 20],事务 B 锁 (15, 25]),可能因加锁顺序不同引发死锁
-
INSERT ... ON DUPLICATE KEY UPDATE在唯一键冲突时会先尝试插入,触发间隙锁,再执行更新——这比纯UPDATE更易锁住更大范围 - 显式开启事务后,哪怕只执行一条
SELECT ... FOR UPDATE,间隙锁会一直持有到事务结束(COMMIT或ROLLBACK),不是语句结束就释放
真正难调试的,永远是那个没写 FOR UPDATE 却因为隔离级别和索引隐式带上间隙锁的查询——它安静地挡住了别人,自己还不报错。










