幻读与 Next-Key Lock:可重复读隔离级别如何解决幻读

在上一篇中,我们全面梳理了 InnoDB 的锁分类,认识了记录锁、间隙锁、临键锁和插入意向锁。其中临键锁(Next-Key Lock)被反复提及------它是 InnoDB 在 REPEATABLE READ 隔离级别下默认的行锁形式,也是防止幻读的核心武器。

本文将聚焦于幻读这一特殊的并发问题,深入剖析:

  • 幻读的严格定义与场景再现
  • Next-Key Lock 的工作原理
  • 为什么 RR 级别能通过它防止大部分幻读
  • 幻读与 Serializable 隔离级别的对比
  • 实战:演示幻读的发生与 Next-Key Lock 的阻止效果

读完本文,你将能清晰解释"InnoDB 默认隔离级别为什么能防幻读",并理解其实现机制。


1. 再识幻读:同一个事务,不同的结果集

回顾一下三类并发问题:

  • 脏读:读到未提交的数据。
  • 不可重复读:同一行被修改并提交,两次读的值不同。
  • 幻读 :同一范围查询,两次返回的行数不同。

幻读的"幻"在于,第二次查询时莫名其妙多出了几行(或少了),就像出现了幻觉。它通常由其他事务的 INSERTDELETE 引起,不是修改已有行,而是改变结果集的大小

典型场景

  • 事务 T1 查询"所有余额大于 500 的账户",返回 3 行。
  • 事务 T2 插入一行余额为 600 的新账户,并提交。
  • 事务 T1 再次执行同一查询,返回 4 行。

如果 T1 基于第一次查询的结果做了汇总计算(比如 SUM),就会发现两次总和不一致。这种不一致可能导致业务逻辑出错。


2. Next-Key Lock:行锁 + 间隙锁的合体

2.1 行锁为什么不够?

普通的记录锁 只锁定已存在的行 。如果 T1 对 balance > 500 的所有现有行加了行锁,T2 仍然可以插入一条新的 balance = 600 的记录------因为这条记录还不存在,没有任何锁阻止它。于是幻读依然发生。

要阻止插入,必须锁住行与行之间的"间隙"。这就是间隙锁(Gap Lock)的作用。

2.2 Next-Key Lock = Record Lock + Gap Lock

Next-Key Lock 锁定的是一个左开右闭 的区间 (a, b],包含:

  • 对该区间内已有记录的记录锁(防修改)
  • 对这些记录之间间隙的间隙锁(防插入)

它相当于在索引上"画地为牢",把范围查询锁住的每一段间隙都保护起来。

示例 :假设表中有索引记录 10, 20, 30。执行:

sql 复制代码
SELECT * FROM t WHERE id BETWEEN 15 AND 25 FOR UPDATE;

InnoDB 会加以下 Next-Key Locks:

  • (10, 20] 区间(覆盖了 15~20)
  • (20, 30] 区间(覆盖了 20~25)

此外,还会加上间隙锁 (20, 25) 以及可能的一些额外锁,实际上会锁住 (10, 20](20, 30],有效阻止其他事务在 (10, 30) 间插入新行,以及修改 2030

2.3 如何防止幻读?

在上述例子中,如果 T2 试图插入 id = 18id = 25,插入意向锁会试图在 (10, 20)(20, 30) 间隙上加锁,但这些间隙已被 T1 的 Next-Key Lock 保护,插入意向锁会被阻塞。因此 T2 无法在 T1 的两次查询之间插入新行,幻读就此被阻止。


3. 隔离级别与 Next-Key Lock 的关系

  • READ COMMITTED:不使用间隙锁,只使用记录锁。因此无法防止幻读。
  • REPEATABLE READ:默认使用 Next-Key Lock,防止幻读。
  • SERIALIZABLE :最严格,所有读操作都隐式加共享锁(类似 SELECT ... FOR SHARE),所有读写完全串行化,自然也防幻读。

InnoDB 的 RR 通过 Next-Key Lock 在大部分场景下阻止了幻读,但并非绝对。某些边缘情况(如先快照读后当前读,或者不同索引条件)仍可能出现类似幻读的现象。这点在后续 MVCC 篇会结合 ReadView 深入探讨。

Serializable 与 RR 的区别

  • Serializable 强制所有读取都加锁,导致读读也可能阻塞(如果使用 FOR UPDATE 的语义),并发度极低。
  • RR 下的普通 SELECT 是无锁的快照读(通过 MVCC),只有显式加锁的 SELECT ... FOR UPDATE/SHARE 或 DML 操作才会使用 Next-Key Lock。因此并发度远高于 Serializable。

4. 实战:演示幻读与 Next-Key Lock 的阻止

我们来实际观察 RC 下的幻读现象,以及 RR 下 Next-Key Lock 如何防幻读。

4.1 准备测试表

sql 复制代码
USE library_db;

CREATE TABLE phantom_test (
    id INT PRIMARY KEY,
    name VARCHAR(20)
) ENGINE=InnoDB;

INSERT INTO phantom_test VALUES (10, 'A'), (20, 'B'), (30, 'C');

4.2 READ COMMITTED 下的幻读

会话 A

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- 第一次查询
SELECT * FROM phantom_test WHERE id BETWEEN 15 AND 25;
-- 返回:Empty set(无记录)

会话 B

sql 复制代码
INSERT INTO phantom_test VALUES (18, 'phantom');
COMMIT;

会话 A(继续同一事务):

sql 复制代码
-- 第二次查询
SELECT * FROM phantom_test WHERE id BETWEEN 15 AND 25;
-- 返回:id=18(幻读!)
COMMIT;

因为 RC 下没有间隙锁,B 插入成功,A 看到了新行。

4.3 REPEATABLE READ 下 Next-Key Lock 的阻止

会话 A

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM phantom_test WHERE id BETWEEN 15 AND 25 FOR UPDATE;
-- 加锁区间(10,20] 和 (20,30]

会话 B

sql 复制代码
INSERT INTO phantom_test VALUES (18, 'blocked');
-- 会阻塞!因为 18 落在 (10,20) 间隙中

此时会话 B 会等待锁,直到会话 A 提交或回滚。如果等待超时,会报错:

复制代码
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

会话 A

sql 复制代码
COMMIT;

会话 B 会在 A 提交后立即插入成功。这完美展示了 Next-Key Lock 对幻读的阻止效果。

4.4 查看锁信息

在阻塞期间,可以在第三个会话中查看锁情况:

sql 复制代码
SELECT
    lock_type, lock_mode, lock_data, lock_status
FROM performance_schema.data_locks
WHERE object_name = 'phantom_test';

你会看到 X,GAPX 等锁模式,以及被锁定的索引记录和间隙。

4.5 清理

sql 复制代码
DROP TABLE phantom_test;

5. Next-Key Lock 的局限与代价

虽然 Next-Key Lock 很有力,但并非完美:

  • 锁范围可能扩大:如果 WHERE 条件无法使用精确索引,导致扫描全表,Next-Key Lock 会锁住整个索引的所有间隙和记录,接近于表锁。
  • 影响并发插入:被锁住的间隙内,其他事务的 INSERT 会被阻塞,可能导致写入性能下降。
  • 死锁风险增加:多个事务各自持有一些间隙锁,又等待对方释放,形成死锁。

因此,在业务层设计查询时,要尽量确保 WHERE 条件能走合适的索引,避免大范围扫描导致的锁膨胀。


6. 小结

本文围绕幻读与 Next-Key Lock 进行了深入探讨:

  • 幻读:同一事务内两次范围查询结果集的行数变化,由其他事务插入/删除引起。
  • Next-Key Lock = 记录锁 + 间隙锁,锁定左开右闭区间,是 InnoDB 在 RR 级别防止幻读的核心机制。
  • RC vs RR:RC 无间隙锁,存在幻读;RR 使用 Next-Key Lock 阻止插入,实现防幻读。
  • Serializable:通过强制读锁将并发度降到最低,完全防幻读,但代价巨大。
  • 实战:亲手在 RC 下再现幻读,在 RR 下用 Next-Key Lock 成功阻止。

通过本文,你应该对"InnoDB 如何解决幻读"有了直观且深入的理解。下一篇我们将进入另一个并发难题------死锁的产生、检测与避免,学习如何从日志中诊断死锁,以及如何通过设计规范避免它。

思考题

  1. 为什么在 RC 隔离级别下 UPDATE 也可能引发幻读?举个例子。
  2. 如果 phantom_test 表上没有索引,SELECT ... FOR UPDATE 会加什么锁?
  3. 尝试修改事务隔离级别为 SERIALIZABLE,执行相同的测试,观察锁等待现象。

参考资料


相关推荐
C137的本贾尼1 小时前
死锁的产生、检测与避免
数据库
C137的本贾尼1 小时前
事务入门:确保数据的一致性与持久性
数据库
郑洁文1 小时前
达州市人口相关数据分析与应用
大数据·数据挖掘·数据分析·毕设·达州市人口
我爱吃土豆11 小时前
Agent 的记忆机制
开发语言·数据库·人工智能
AOwhisky1 小时前
MySQL 学习笔记(第五期):用户管理与权限控制
linux·运维·数据库·笔记·学习·mysql
YangYang9YangYan1 小时前
2026文科生报考大数据类专业学习数据分析的可行性分析
大数据·学习·数据分析
梦想的颜色1 小时前
Redis数据类型全解析:从底层原理到生产实战
运维·数据库·redis·缓存·高并发·分布式锁·数据类型
知识分享小能手1 小时前
Hadoop学习教程,从入门到精通, 初识Hadoop — 知识点详解(1)
大数据·hadoop·学习
weixin_408318041 小时前
2026年医疗直播行业趋势报告:技术方向、监管变化与市场格局
java·大数据·人工智能