临键锁(Next-Key Lock):解决幻读的核心机制
你想了解临键锁如何解决幻读问题,这是 InnoDB 并发控制的核心知识点 —— 幻读的本质是 “同一事务内,相同查询在不同时间返回不同行数”,而临键锁通过锁定记录 + 记录间隙的方式,从根本上阻止了其他事务插入 / 修改导致的 “幻觉数据”,结合 MVCC 最终彻底解决幻读。
下面我会从「幻读的根源」→「临键锁的定义」→「临键锁解决幻读的原理」→「实战案例」逐步拆解,让你直观理解这个机制。
一、先明确:幻读的核心根源
幻读只发生在REPEATABLE READ(可重复读)级别以下(SQL 标准中),其产生的核心原因是:
- 事务 A 执行范围查询(如
SELECT * FROM user WHERE age = 20),仅锁定了已存在的符合条件的记录; - 事务 B 插入一条
age=20的新记录并提交; - 事务 A 再次执行相同查询,结果行数变多(出现 “幻觉”),若此时事务 A 执行
UPDATE user SET name='新' WHERE age=20,会意外修改事务 B 插入的记录,破坏事务隔离性。
关键:普通行锁只能锁定已存在的记录,无法阻止 “插入新记录”,这是幻读的核心漏洞。
二、临键锁(Next-Key Lock)的基础定义
1. 核心概念
临键锁是 InnoDB 在REPEATABLE READ级别下默认使用的锁类型,是行锁 + 间隙锁(Gap Lock)的组合:
- 行锁:锁定表中已存在的具体记录(如
age=20的 id=1 这条记录); - 间隙锁:锁定两条索引记录之间的 “空白区域”(如
age=19和age=20之间的间隙、age=20和age=21之间的间隙),阻止插入新记录。
2. 临键锁的锁定范围
InnoDB 的索引是有序的(B + 树),临键锁会锁定当前记录到下一条记录的左闭右开区间。示例:假设 user 表的 age 字段有索引,且存在值:18、20、22,那么 age 索引的临键锁区间为:
- (-∞, 18]
- (18, 20]
- (20, 22]
- (22, +∞)
三、临键锁解决幻读的核心原理
临键锁通过 “锁定查询涉及的所有临键区间”,实现两个核心效果:
- 阻止其他事务插入间隙内的新记录:间隙锁会禁止其他事务在锁定的区间内插入任何数据,从根源上杜绝 “新数据导致行数变化”;
- 阻止其他事务修改已锁定的记录:行锁会禁止其他事务修改已存在的符合条件的记录;
- 结合 MVCC(多版本并发控制):事务内的读操作基于快照,即使其他事务修改了未锁定的数据,也不会影响当前事务的查询结果。
原理总结(一句话):
临键锁把 “查询条件涉及的记录 + 可能插入新记录的间隙” 全部锁住,让其他事务既不能修改已有数据,也不能插入新数据,因此当前事务的多次相同查询结果完全一致,彻底解决幻读。
四、实战案例:临键锁如何阻止幻读
为了直观理解,我们用两个事务的交互过程演示(基于REPEATABLE READ级别):
准备环境
-- 创建表并插入数据 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, age INT, name VARCHAR(20), INDEX idx_age (age) -- 必须加索引,临键锁基于索引生效 ); INSERT INTO user (age, name) VALUES (20, '张三'), (22, '李四');事务执行过程
| 时间 | 事务 A(隔离级别:REPEATABLE READ) | 事务 B |
|---|---|---|
| T1 | BEGIN; -- 开启事务 | - |
| T2 | -- 执行范围查询,触发临键锁 | - |
| SELECT * FROM user WHERE age = 20 FOR UPDATE; | - | |
| T3 | -- 查询结果:只有 age=20 的张三 | BEGIN; -- 开启事务 |
| T4 | - | -- 尝试插入 age=20 的新记录 |
| INSERT INTO user (age, name) VALUES (20, ' 王五 '); | ||
| T5 | - | -- 插入被阻塞(临键锁生效),事务 B 进入等待状态 |
| T6 | -- 再次执行相同查询,结果仍只有张三(无幻读) | - |
| SELECT * FROM user WHERE age = 20; | ||
| T7 | COMMIT; -- 事务 A 提交,释放临键锁 | - |
| T8 | - | -- 事务 B 的插入操作才执行成功(等待结束) |
关键分析
- T2 步骤:事务 A 执行
SELECT ... FOR UPDATE(加锁读),InnoDB 会为age=20触发临键锁,锁定的区间是(18, 20](假设 age=18 是前一条记录); - T4-T5 步骤:事务 B 尝试插入
age=20的记录,正好落在(18, 20]的间隙内,被间隙锁阻止,无法插入; - T6 步骤:事务 A 再次查询,由于没有新记录插入,结果和第一次一致,幻读被彻底阻止;
- 核心:临键锁不仅锁住了已存在的
age=20记录(行锁),还锁住了age=20附近的间隙(间隙锁),让其他事务无法插入新的age=20记录。
五、临键锁的特殊情况(补充说明)
1. 临键锁的降级
如果查询条件是主键 / 唯一索引的等值查询,且查询的记录存在,InnoDB 会将临键锁降级为行锁(只锁记录,不锁间隙),减少锁范围,提升并发:
-- 主键等值查询,记录存在,临键锁降级为行锁 SELECT * FROM user WHERE id = 1 FOR UPDATE; -- 只锁id=1的记录,不锁间隙2. 临键锁仅在REPEATABLE READ级别生效
READ COMMITTED级别下,InnoDB 会关闭间隙锁(仅保留行锁),临键锁降级为行锁,因此该级别仍会出现幻读;SERIALIZABLE级别下,所有查询都会隐式加FOR SHARE,临键锁范围更大,完全串行执行,无幻读但性能极差。
3. 无索引时的临键锁
如果查询的字段没有索引,InnoDB 会对整个表的所有间隙加锁(表级锁效果),导致所有插入操作都被阻塞,性能极差 —— 这也是 “查询字段必须加索引” 的重要原因。
六、临键锁 vs MVCC:共同解决幻读
需要注意的是,InnoDB 解决幻读是 “临键锁 + MVCC” 的组合拳:
- 临键锁:针对写操作(插入 / 修改),阻止其他事务插入新记录或修改已有记录;
- MVCC:针对读操作(普通 SELECT),事务内的读基于快照,即使其他事务提交了修改,当前事务也看不到,保证 “可重复读”。
二者结合,既阻止了 “写操作导致的行数变化”,又保证了 “读操作的一致性”,最终彻底解决幻读。
总结
- 幻读的根源:普通行锁只能锁定已有记录,无法阻止插入新记录,导致同一事务查询行数变化;
- 临键锁的核心逻辑:通过 “行锁(锁已有记录)+ 间隙锁(锁空白区间)”,阻止其他事务修改已有记录、插入新记录;
- 解决幻读的关键:临键锁锁定了查询条件涉及的所有可能插入新记录的间隙,让 “幻觉数据” 无法产生,结合 MVCC 最终实现无幻读的可重复读。