Appearance
间隙锁与幻读-锁住的是我们之间的距离
内容来源:本篇为学习整理,综合自一篇小红书风格笔记的比喻框架("间隙锁,锁住的是我们之间的距离")与 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/DELETE | WHERE 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 尾部热点间隙的业务侧序列化。
结论:「一锁」等值唯一索引在记录存在时是模板的安全形态;在记录不存在时静默退化为间隙锁,与并发插入组合成死锁。
五、死锁:间隙锁的高发陷阱
间隙锁是死锁的高发因素。一个经典场景:
- 事务 A 和事务 B 都试图在
(10, 20)间隙内插入记录(如插入 15 和 16); - 两者都先获取
(10, 20)的间隙锁——由于间隙锁互相兼容,都成功; - 事务 A 尝试插入 15:持有插入意向锁,被事务 B 的间隙锁阻塞;
- 事务 B 尝试插入 16:同样被事务 A 的间隙锁阻塞;
- 两个事务互相等待对方释放间隙锁——死锁形成。
排查与缓解工具:
SHOW ENGINE INNODB STATUS——查看LATEST DETECTED DEADLOCK段的死锁日志(谁持有什么、等什么);innodb_lock_wait_timeout——锁等待超时(默认 50s),缓解长时间阻塞;- 业务层面:事务内避免先共享读再插入的交叉模式,统一插入顺序。
六、最佳实践
- 确保查询走索引:无索引时 InnoDB 全表扫描并对所有间隙加锁——性能灾难 + 并发雪崩;
- 控制事务大小:间隙锁生命周期与事务一致,长事务长时间持有间隙锁阻塞插入——大事务拆小事务;
- 谨慎
SELECT ... FOR UPDATE:仅必要时使用,普通一致性读走 MVCC 快照即可; - 隔离级别调整:业务能容忍幻读时用读已提交(RC)——该级别下 InnoDB 不用间隙锁,并发性能显著提升(很多大厂生产默认 RC 的原因)。
追问链
- 间隙锁锁的是什么?→ 索引记录之间的间隙,不是数据行;唯一目的是阻止插入;
- 幻读和不可重复读的区别?→ 不可重复读是已有行的值变了(update),幻读是行数变了(insert)——间隙锁只防后者;
- 为什么唯一索引命中时不加间隙锁?→ 唯一性保证该值不可能再出现,没有"插进间隙"的风险,临键锁退化为记录锁;
- 间隙锁之间为什么兼容?→ 它们的目的都是"禁止插入",读与读不冲突;冲突的是插入动作本身(插入意向锁);
- RC 级别下为什么没有间隙锁?→ RC 允许幻读,不需要防插入的锁机制——这是大厂生产默认 RC 的核心原因(并发优先);
- 间隙锁死锁怎么排查?→
SHOW ENGINE INNODB STATUS的 LATEST DETECTED DEADLOCK 段看锁等待图,业务上统一插入顺序。
面试官视角小结
间隙锁题的区分度在三层递进:说出"锁间隙防幻读"是背过;讲清"间隙锁互相兼容、与插入意向锁互斥"的理解了;能主动推演"两个事务同间隙加锁后互插形成死锁"的场景、并给出 RC 级别与统一插入顺序的解法的,是真正处理过锁问题的。最高频的坑题是"唯一索引等值命中加什么锁"——答临键锁的是没读懂退化规则,答记录锁的才算入门。再追问"RC 为什么不用间隙锁",能从并发优先与幻读容忍的业务权衡讲起,就是有生产视角的候选人。
2026-11 新增(MySQL 锁体系补充篇;学习整理综合自一篇小红书风格笔记与 InnoDB 官方语义,见文首来源块)。姊妹篇:MySQL锁详解、脏读、幻读、不可重复读区别。