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

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

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

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

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


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

回顾一下三类并发问题:

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

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

典型场景:

  • 事务 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) 间插入新行,以及修改 20 和 30。

2.3 如何防止幻读?

在上述例子中,如果 T2 试图插入 id = 18 或 id = 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,GAP 或 X 等锁模式,以及被锁定的索引记录和间隙。

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,执行相同的测试,观察锁等待现象。

参考资料


相关推荐
kingx35 小时前
Windows Server 2008 应急加固实战:7 项入侵痕迹排查与修复
数据库·windows·提权·应急响应·加固
xiaogai_gai5 小时前
采购业务退款单同步实战:从钉钉审批流到金蝶云星空付款单
java·数据库·钉钉
Fluxproxy6 小时前
大模型批量数据采集:AI 自动化工作流搭建实战
大数据·人工智能·自动化
sun153690275726 小时前
超声波风速风向传感器:测量原理、通讯协议与应用场景详解
大数据
AlickLbc6 小时前
DM-V8数据库证书过期处理过程记录
数据库
l1t6 小时前
DeepSeek总结的五分钟测试:Ted Dunning 谈 DuckDB 为何让分析更轻松
数据库·sql·duckdb
国科安芯6 小时前
商业光学遥感载荷焦平面供电链的低噪声稳压器研究综述——以国科安芯ASP7A84AS型3A低压差线性稳压器为器件级参照
大数据·网络·嵌入式硬件·平面·卫星·商业航天·抗辐射
超级新零售7 小时前
零售 SaaS 与本地部署,技术责任边界究竟怎么分?
数据库·零售
米软科技7 小时前
数字化转型:多元化集团如何构建高效的AI数字化中台?
大数据·人工智能
Leo.yuan7 小时前
Dora企业级Data Agent应用指南:存量BI复用、私有化部署与权限管理
大数据·人工智能