Skip to content

间隙锁与幻读-锁住的是我们之间的距离 ​

内容来源:本篇为学习整理,综合自一篇小红书风格笔记的比喻框架("间隙锁,锁住的是我们之间的距离")与 InnoDB 官方语义(版权归原作者所有);技术细节经与本库MySQL锁详解对照校准,表述为本库原创加工。

看在前面 ​

这篇讲 InnoDB 在可重复读隔离级别下防幻读的核心武器——间隙锁(Gap Lock)。它锁定的不是具体的数据记录,而是索引记录之间的"空白地带"——一篇小红书笔记用"锁住的是我们之间的距离"这个比喻精准点破了它的本质。姊妹篇:MySQL锁详解(全局锁/表级锁/行级锁全景)。

一句话定义:workflow 分界线在这里不适用,但工程纪律相同——先搞清楚"锁什么、为什么锁",再谈优化。

一、间隙与幻读:两个名词一张图 ​

间隙(Gap):索引记录之间的空白地带,加上第一条记录之前和最后一条记录之后的区域。假设 age 索引列的值为 10, 20, 30,那么间隙就是:

(-∞, 10)   (10, 20)   (20, 30)   (30, +∞)
  ↑           ↑           ↑           ↑
 小于10     10和20之间   20和30之间   大于30

幻读(Phantom Read):一个事务前后两次相同的范围查询,后一次看到了前一次没有的行——因为其他事务在查询范围内插入了新数据并提交。比如事务 A 查询 age BETWEEN 15 AND 25 并锁定了结果,事务 B 插入一条 age=18 并提交,事务 A 再查同样的范围就会"凭空多出"一条。

概念关系一张图(记录锁与间隙锁的组合关系):

二、间隙锁如何工作:锁间隙,防插队 ​

事务执行范围查询(SELECT ... FOR UPDATE)或等值查询未命中记录时,InnoDB 会在扫描到的索引记录的间隙上加锁,阻止其他事务在这个范围内插入新数据——从而防止幻读。

间隙锁的冲突规则很特殊(面试高频):

  • 兼容:不同事务可以同时持有同一个间隙的间隙锁——多个事务都能"读"这个间隙;
  • 冲突:任何事务在间隙锁锁定的范围内插入时都会被阻塞,直到持有间隙锁的事务提交并释放。

记忆点:间隙锁之间互相兼容,间隙锁与插入意向锁互斥——锁的粒度不是"行",是"往这个范围里插队"这个动作。

三、核心加锁规则:临键锁及其退化 ​

理解间隙锁离不开它的实现基础——临键锁(Next-Key Lock):记录锁与间隙锁的组合,锁定左开右闭区间(如 (10, 20])。InnoDB 加锁的基本单位就是临键锁,在不同场景下会退化为记录锁或纯间隙锁:

场景加锁行为为什么
等值查询 · 唯一索引命中退化为记录锁,仅锁该行唯一索引保证无重复值,不存在"插入"风险
等值查询 · 唯一索引未命中锁定对应间隙记录不存在,防的就是"插进这个位置"
等值查询 · 非唯一索引锁记录及其前面的间隙,继续向右扫描至第一个不满足值,最后一个临键锁退化为间隙锁非唯一索引可能有重复值,防"插进重复区间"
范围查询锁定扫描范围内所有记录及间隙(无论索引唯一性)范围内的任何位置都可能被插入

唯一索引命中时退化为记录锁是这套规则里最优雅的设计——唯一性约束本身就是最好的防插队机制,不需要间隙锁多此一举。

四、触发场景:什么操作会踩中间隙锁 ​

原理之外,工程里真正踩中间隙锁的六个高频场景——每个都对应一类线上事故:

#场景触发动作典型事故形态
1扣库存/下单锁定区间SELECT ... FOR UPDATE 范围条件(如 WHERE amount BETWEEN x AND y)大促热点区间插入被阻塞,下单 RT 飙升
2查询不存在的记录等值查 WHERE id = 不存在的值(判存在再插入的"先查后插"模式)先查后插是间隙锁重灾区——查无结果会锁住整个间隙,并发"判空再插"互相阻塞甚至死锁
3非唯一索引的等值更新UPDATE ... WHERE status = 'INIT'(status 非唯一)锁住所有 INIT 记录及其间隙,批量任务互相卡
4无索引的 UPDATE/DELETEWHERE no_index_col = x全表扫描 → 全表所有间隙加锁——等于锁全表,线上最凶险
5数据修补/订正脚本与在线事务交叉DBA 订正窗口执行大范围 UPDATE阻塞全部相关区间插入,订正窗口内业务 RT 尖刺
6长事务内的范围读事务开了没提交,中间夹范围查询间隙锁持有到事务结束,越长的锁越容易卷进死锁链

对照自查:这六个场景的共同根因是「可重复读 + 当前读(锁读/更新)+ 走了索引或全表」。绕开的手法也是三个维度:改隔离级别(RC)、改实现(先查后插改原子插入 INSERT IGNORE/ON DUPLICATE KEY)、改范围(缩小锁定区间)。

深水区:「一锁二判三操作」模板与间隙锁 ​

企业级代码模板(交易/支付系统的标准姿势):

sql
BEGIN;
SELECT * FROM account WHERE id = ? FOR UPDATE;   -- 一锁
-- 二判:存在 → UPDATE;不存在 → INSERT          -- 三操作
COMMIT;

id 走唯一索引等值查,但记录可能不存在(首次开户/首次下单)——这种情况:

情况加锁行为风险
记录存在临键锁退化为记录锁(唯一性保证无插入风险)✅ 安全
记录不存在无法锁不存在的行 → 锁住落点间隙(纯间隙锁)⚠️ 死锁温床

死锁机理(两事务并发"一锁"同一间隙内的不同 id):

索引:... 100, 200 ...

事务A:SELECT ... WHERE id=150 FOR UPDATE  → 锁住 (100,200) 间隙
事务B:SELECT ... WHERE id=180 FOR UPDATE  → 也锁住 (100,200) 间隙(间隙锁互相兼容)
事务A:INSERT id=150  → 插入意向锁被 B 的间隙锁阻塞
事务B:INSERT id=180  → 插入意向锁被 A 的间隙锁阻塞 → 死锁

绕开手法:插入路径改原子(INSERT IGNORE/ON DUPLICATE KEY UPDATE);前置存在性保证(独立事务先插兜底行,再走"一锁"命中记录锁形态);自增 id 尾部热点间隙的业务侧序列化。

结论:「一锁」等值唯一索引在记录存在时是模板的安全形态;在记录不存在时静默退化为间隙锁,与并发插入组合成死锁。

五、死锁:间隙锁的高发陷阱 ​

间隙锁是死锁的高发因素。一个经典场景:

  1. 事务 A 和事务 B 都试图在 (10, 20) 间隙内插入记录(如插入 15 和 16);
  2. 两者都先获取 (10, 20) 的间隙锁——由于间隙锁互相兼容,都成功;
  3. 事务 A 尝试插入 15:持有插入意向锁,被事务 B 的间隙锁阻塞;
  4. 事务 B 尝试插入 16:同样被事务 A 的间隙锁阻塞;
  5. 两个事务互相等待对方释放间隙锁——死锁形成。

排查与缓解工具:

  • SHOW ENGINE INNODB STATUS——查看 LATEST DETECTED DEADLOCK 段的死锁日志(谁持有什么、等什么);
  • innodb_lock_wait_timeout——锁等待超时(默认 50s),缓解长时间阻塞;
  • 业务层面:事务内避免先共享读再插入的交叉模式,统一插入顺序。

六、最佳实践 ​

  1. 确保查询走索引:无索引时 InnoDB 全表扫描并对所有间隙加锁——性能灾难 + 并发雪崩;
  2. 控制事务大小:间隙锁生命周期与事务一致,长事务长时间持有间隙锁阻塞插入——大事务拆小事务;
  3. 谨慎 SELECT ... FOR UPDATE:仅必要时使用,普通一致性读走 MVCC 快照即可;
  4. 隔离级别调整:业务能容忍幻读时用读已提交(RC)——该级别下 InnoDB 不用间隙锁,并发性能显著提升(很多大厂生产默认 RC 的原因)。

追问链 ​

  • 间隙锁锁的是什么?→ 索引记录之间的间隙,不是数据行;唯一目的是阻止插入;
  • 幻读和不可重复读的区别?→ 不可重复读是已有行的值变了(update),幻读是行数变了(insert)——间隙锁只防后者;
  • 为什么唯一索引命中时不加间隙锁?→ 唯一性保证该值不可能再出现,没有"插进间隙"的风险,临键锁退化为记录锁;
  • 间隙锁之间为什么兼容?→ 它们的目的都是"禁止插入",读与读不冲突;冲突的是插入动作本身(插入意向锁);
  • RC 级别下为什么没有间隙锁?→ RC 允许幻读,不需要防插入的锁机制——这是大厂生产默认 RC 的核心原因(并发优先);
  • 间隙锁死锁怎么排查?→ SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 段看锁等待图,业务上统一插入顺序。

面试官视角小结 ​

间隙锁题的区分度在三层递进:说出"锁间隙防幻读"是背过;讲清"间隙锁互相兼容、与插入意向锁互斥"的理解了;能主动推演"两个事务同间隙加锁后互插形成死锁"的场景、并给出 RC 级别与统一插入顺序的解法的,是真正处理过锁问题的。最高频的坑题是"唯一索引等值命中加什么锁"——答临键锁的是没读懂退化规则,答记录锁的才算入门。再追问"RC 为什么不用间隙锁",能从并发优先与幻读容忍的业务权衡讲起,就是有生产视角的候选人。

2026-11 新增(MySQL 锁体系补充篇;学习整理综合自一篇小红书风格笔记与 InnoDB 官方语义,见文首来源块)。姊妹篇:MySQL锁详解、脏读、幻读、不可重复读区别。