在 MySQL InnoDB 中,插入意向锁(Insert Intention Lock)常常让人困惑:它和间隙锁到底什么关系?为什么被阻塞时只有插入意向锁而没有行锁?当阻塞解除时,唤醒顺序又是怎样的?本文将从 B+ 树的有序性出发,结合锁管理器机制,彻底讲清楚插入意向锁的工作原理,并纠正一个非常普遍的误区。
一、核心误区:插入意向锁是「等待状态」,不是「持有状态」
很多同学会误以为:事务 B 被间隙锁挡住时,它已经“持有”了插入意向锁。但事实恰恰相反:
事务 B 的插入意向锁只是一个“我想在这里插入,正在等”的等待标记,它没有真正持有任何锁,因此无法主动释放。
这个标记只会在两种情况下消失:
- 事务 B 被回滚:锁管理器直接清理它的等待标记,把它从等待队列中移除;
- 事务 A 释放间隙锁,事务 B 被唤醒:它成功拿到插入意向锁,完成插入后,锁自动释放。
明白这一点,是理解后面所有行为的基础。
二、为什么只检查前后两个节点,就能判断所有间隙锁冲突?
当事务 B 要插入id=4(假设表中有记录3和10,间隙锁区间为(3,10))时,InnoDB 并不会去遍历全表的锁信息,而是利用B+ 树的有序性,以O(log n)定位到插入点的前驱节点和后继节点(此处为3和10),然后只检查这两个节点上的间隙锁。
2.1 原理:间隙锁区间连续且不重叠
在 InnoDB 中,间隙锁的区间是按顺序排好的,例如:
(1,3) 、 (3,10) 、 (10,15)这些区间连续且不重叠,插入点id=4只能属于一个间隙锁区间,而这个区间的边界,一定就是它的前序节点(3)和后继节点(10)。
因此,你只需要检查3和10这两个节点上的间隙锁,就能100% 确定id=4有没有被挡住,完全不用关心id=1、id=15等无关节点,更不需要遍历全表。
2.2 一个通俗的比喻
你要去 4 号房间,走廊上的房间按 3、10 排好(中间没有其他房间)。
你根本不用把整个走廊所有房间都看一遍,只需要看前一个房间(3 号)和后一个房间(10 号),就能知道这段路有没有被封。
三、插入意向锁的真正作用:把“节点定位 + 间隙锁检查”变成 O(1) 的冲突缓存
插入意向锁的核心价值,在于给后续的插入请求做“冲突缓存”,避免它们重复进行节点定位和间隙锁检查。
3.1 工作流程(示例:间隙锁(3,10),事务 B 插入4被阻塞)
- 事务 B要插入
id=4,第一次被(3,10)间隙锁挡住,在锁管理器中生成一个插入意向锁(状态 = 等待中); - 事务 C也要插入
id=4,它不用再去定位3和10节点、再检查间隙锁,直接在锁管理器里看到“id=4已经有一个等待的插入意向锁”,就知道这里冲突了,直接排队; - 事务 D要插入
id=5(仍在同一间隙内),定位前后节点3和10时,发现间隙锁(3,10)依然存在,并且锁管理器中已有针对该间隙的等待标记,从而快速判断冲突,无需额外遍历锁队列,直接进入等待状态。
3.2 一句话总结
插入意向锁把重复的“节点定位 + 间隙锁检查”,变成了一次O(1)的锁状态查询,大幅提升了并发插入的性能。
四、间隙锁与插入意向锁的冲突判断:基于「区间包含」的 O(1) 操作
InnoDB 的锁管理器,对间隙锁和插入意向锁的冲突判断,是基于“区间是否包含”的:
- 间隙锁的区间是
(a, b),插入意向锁的点是x; - 只需要判断
a < x < b,就能确定是否冲突。
这是一个O(1)的操作,与遍历完全无关。这也是为什么 InnoDB 能在高并发插入场景下依然保持高性能的原因之一。
五、阻塞解除全流程:事务 A 提交后,到底发生了什么?
这是大家最关心的部分。假设当前场景:
- 表中有记录
id=3和id=10,事务 A 持有间隙锁(3,10); - 事务 B(插入
id=4)、事务 C(插入id=4)、事务 D(插入id=5)依次进入等待队列。
当事务 A 提交,释放间隙锁(3,10)时,锁管理器会按FIFO(先进先出)顺序唤醒等待队列中的事务:
| 步骤 | 动作 | 结果 |
|---|---|---|
| 1 | 事务 A 提交,释放间隙锁(3,10) | 路解封,锁管理器开始处理等待队列 |
| 2 | 按顺序唤醒第一个事务:事务 B(id=4) | 事务 B 被唤醒,重新尝试获取插入意向锁 |
| 3 | 事务 B 获取插入意向锁,执行 INSERTid=4 | 无冲突,插入成功;插入完成后,插入意向锁立即释放 |
| 4 | 锁管理器唤醒下一个事务:事务 C(id=4) | 事务 C 被唤醒,尝试插入id=4,触发主键唯一约束冲突(B 已经插入了),直接报错失败,等待标记被清理 |
| 5 | 锁管理器唤醒下一个事务:事务 D(id=5) | 事务 D 被唤醒,此时表中有记录3,4,10,插入id=5无主键冲突、无间隙锁冲突,获取插入意向锁,插入成功后释放锁 |
| 6 | 等待队列清空,所有阻塞解除 | 后续事务可以正常插入(3,10)区间内的任意整数值 |
关键点说明
- 插入意向锁持有时间极短:事务 B 一旦插入成功,锁立即释放,不会阻塞后面的事务 C。
- 事务 C 的失败与锁无关:它被唤醒后失败,是因为主键冲突,而不是因为锁被事务 B 持有了。
- 如果事务 B 在等待期间被回滚(比如超时),它的等待标记会被直接清理,不会影响队列中其他事务的等待状态。
六、插入意向锁 vs 行级排他锁:一句话讲清区别
很多同学不清楚,插入成功后到底持有的是什么锁。我们用一句话讲死:
插入被阻塞排队时:只有插入意向锁(等待标记),没有行锁(数据还没插进来,根本没行可锁)。
一旦插入成功:插入意向锁立刻消失,马上给新插入的这一行加上行级排他锁(记录锁)。
| 阶段 | 持有锁类型 | 作用 |
|---|---|---|
| 等待间隙锁释放 | 插入意向锁(等待状态) | 标记“我要在这里插入”,与间隙锁冲突 |
| 插入成功瞬间 | 行级排他锁(记录锁) | 保护新行,禁止别人 update/delete,但允许插入其他空位 |
所以:
- 插入意向锁:管“能不能插进来”,是跟间隙锁打架用的;
- 行级排他锁:管“插进来之后别人能不能改、删这条数据”。
七、插入意向锁与间隙锁的另一个重要差异
尽管插入意向锁也属于一种间隙锁,但两个事务不能在同一时间内,一个持有间隙锁,另一个持有该间隙区间内的插入意向锁。如果插入意向锁不在间隙锁区间内,则允许共存。
这个特性确保了:
- 间隙锁用于保护范围不被插入;
- 插入意向锁用于表示“我想在这个范围插入,正在排队”;
- 两者在同一个区间内互斥,从而实现了可序列化级别的并发控制。
八、总结
- 定位高效:利用 B+ 树有序性,只需检查前后两个节点上的间隙锁,O(log n) 定位 + O(1) 冲突检查。
- 冲突缓存:插入意向锁让后续并发插入请求可以快速判断冲突,避免重复的节点定位和间隙锁检查。
- 等待而非持有:被阻塞时,插入意向锁只是等待标记;插入成功后立即消失,取而代之的是行级排他锁。
- FIFO 唤醒:间隙锁释放后,按先进先出顺序唤醒等待队列中的事务;后续事务可能因主键冲突而失败,这与锁无关。
理解这些机制,不仅能够帮助你更好地设计高并发下的数据表结构,也能让你在排查死锁、锁等待问题时游刃有余。