为什么MySQL可重复读能避免大部分幻读:MVCC与间隙锁的配合

键盘旁的猫
键盘旁的猫
发布时间:2026-09-20| 3人浏览过
原创
按照 SQL-92 标准的最初定义,可重复读(Repeatable Read,简称 RR)隔离级别只承诺解决脏读和不可重复读,幻读的彻底解决需要依赖串行化(Serializable)。然而在 MySQL 的默认存储引擎 InnoDB 中,可重复读级别在日常使用中几乎不会让人感知到幻读。这是因为 InnoDB 划分了“快照读”与“当前读”两类操作,分别借助多版本并发控制(MVCC)与间隙锁(Gap Lock / Next-Key Lock)构建防线,阻断了绝大部分幻读场景的发生。

认识幻读的本质:结构性变化而非数据更新

许多开发者容易把“不可重复读”与“幻读”混淆。不可重复读针对的是已有记录的内容修改或删除,事务前后读取同一行记录,其字段值不一致;而幻读的核心是“集合结构的改变”,即事务在相同查询条件下两次检索,发现凭空多出了符合条件的行。

考虑这样一个场景:事务 A 查询表内所有 age > 18 的用户,返回了 3 条记录。随后事务 B 插入了一条 age = 20 的新记录并提交。如果事务 A 再次执行相同查询并查到了 4 条记录,这种“像见鬼一样凭空冒出来的整行新增”才被称为幻读。

SVG 配图 1
图 1:不可重复读与幻读对比图(不可重复读改变单行值,幻读改变结果集数量)

快照读与当前读:分工明确的两套机制

在深入理解技术实现之前,必须理清 InnoDB 中两类读取行为的差别:

  • 快照读(Snapshot Read):指最常见的非阻塞纯查询,例如不加任何锁修饰的 SELECT * FROM t WHERE id > 10;。快照读不加锁,完全依靠 MVCC 读取历史版本数据。
  • 当前读(Current Read):指获取目标行最新提交数据的读取操作,同时对读取的记录或间隙加锁。例如 SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE(或 MySQL 8.0 的 FOR SHARE),以及所有的 UPDATE、DELETE、INSERT 操作。

由于这两种读操作的机制不同,InnoDB 采取了“分而治之”的防线策略:快照读依靠 MVCC 的一致性视图化解幻读;当前读依靠 Next-Key Lock 锁住范围区间拦截幻读。

第一道防线:MVCC 如何阻断快照读的幻读

MVCC(Multi-Version Concurrency Control)的核心思想是利用数据的多版本链条,使读写操作无需互斥。在 InnoDB 中,每行数据除业务列外,还隐藏了两个重要系统列:trx_id(记录最近修改本行的事务 ID)和 roll_pointer(回滚指针,指向 undo log 中的前一个历史版本)。

Read View 的判定逻辑

在可重复读隔离级别下,一个事务只在执行第一次 SELECT 语句时生成一个 Read View(一致性读视图),且该视图在事务提交前始终保持不变。Read View 包含四个核心属性:

  • m_ids:创建该视图时,系统中当前正在活跃且未提交的事务 ID 列表。
  • min_trx_id:创建视图时活跃事务的最小事务 ID。
  • max_trx_id:创建视图时系统准备分配给下一个事务的 ID(非当前已分配的最大 ID)。
  • creator_trx_id:创建该 Read View 的事务自身 ID。

当查询遍历某行记录的版本链时,InnoDB 按照以下四个推导步骤判断版本可见性:

SVG 配图 2
图 2:MVCC 版本链推导判定步骤(确保快照读始终看到固定的历史切片)

在快照读机制下,即便其他事务中途插入了新行并提交,由于该新行的 trx_id 必然大于事务 A 生成 Read View 时的 max_trx_id(或属于未提交的活跃事务),判定规则会判定该行版本对事务 A 不可见。因此在整个事务期间,无论重复执行多少次普通 SELECT,查询到的数据集行数保持恒定,快照读下的幻读被完全避免。

第二道防线:间隙锁与临键锁如何阻断当前读的幻读

快照读依靠历史切片解决问题,但当业务需要执行“当前读”(如 SELECT ... FOR UPDATE 或 UPDATE)时,系统不能再读历史快照,而必须读取最新的现实数据。这时,仅靠行锁(Record Lock)是防不住幻读的。

假设表中存在两行主键记录:id = 5 与 id = 10。事务 A 执行:

SELECT * FROM t WHERE id > 4 AND id < 11 FOR UPDATE;

如果数据库仅对已存在的 id = 5 和 id = 10 施加行锁,那么其他事务仍然可以成功插入 id = 7 的记录。当事务 A 再次执行相同范围加锁读时,就会发现多出 id = 7 的新行,出现当前读幻读。此外,在主从复制基于 Binlog Statement 格式同步时,并发写入还会导致主从数据状态失衡。

Next-Key Lock 的构成

为解决当前读的数据缝隙问题,InnoDB 引入了间隙锁(Gap Lock)。间隙锁锁定的是两个索引记录之间的“空隙”,防止其他事务在间隙中插入数据。而临键锁(Next-Key Lock)则是行锁与前侧间隙锁的组合体,呈现前开后闭的区间状态 (a, b]。

SVG 配图 3
图 3:间隙锁与临键锁示意图(通过锁定记录之间的开闭区间,彻底阻断新行插入)

当事务 A 在可重复读级别下针对条件 id > 4 AND id < 11 FOR UPDATE 加锁检索时,InnoDB 会在满足条件的区间加上 (5, 10] 的 Next-Key Lock,以及 (10, supremum] 的间隙锁定。此时若事务 B 尝试执行 INSERT INTO t VALUES (7, ...),存储引擎会检测到目标写入点落入已被占用的间隙区间内,自动将事务 B 挂起等待锁释放。间隙内无法产生新记录,当前读的幻读现象自然被有效杜绝。

为什么说是“避免大部分”,而非“彻底杜绝”?

官方文档与实际工程中之所以严谨地表述为“避免大部分幻读”,是因为在某些快照读与当前读混用的特殊时序下,用户依然可以观察到幻读。了解这些边界情况有助于规避线上隐蔽的逻辑缺陷。

典型边界场景:更新了其他事务新增的行

假设数据表初始包含两行数据:id = 1 与 id = 5。两个事务按如下时序交替执行:

时序 事务 A(隔离级别:RR) 事务 B
1 BEGIN;
SELECT * FROM t WHERE id = 3;
(快照读,结果返回空)
2 BEGIN;
INSERT INTO t VALUES (3, 'Bob');
COMMIT;
3 UPDATE t SET name = 'Alice' WHERE id = 3;
(当前读:Affects 1 row!)
4 SELECT * FROM t WHERE id = 3;
(快照读,查到了 id=3 的记录!)

在第 1 步时,事务 A 生成了 Read View,当时数据表中没有 id = 3。事务 B 在第 2 步插入并提交。关键在第 3 步:事务 A 执行了 UPDATE。由于 UPDATE 属于当前读,它会寻找物理存储上的最新记录,成功把 id = 3 的行修改为了 name = 'Alice'。

此时,该行的系统隐藏列 trx_id 被重写为了事务 A 自己的事务 ID。当事务 A 在第 4 步再次执行快照读时,命中 MVCC 规则第一条——“由本事务自身产生或修改的记录对本事务可见”。于是,原本查不到的 id = 3 重新出现在结果集中,形成了幻读。

架构全貌与协同防线总结

通过组合软件设计的轻量化快照与底层排他锁,MySQL 在并发性能与数据一致性之间取得了均衡:

SVG 配图 4
图 4:MVCC 与间隙锁的职能分工与协同总结
工程实践建议
  1. 避免在长事务中混用快照读与当前读:如果一个事务内先用普通 SELECT 做条件校验,随后执行 UPDATE,很容易撞上不可见数据的锁冲突或产生意料之外的数据修改。
  2. 保持查询条件走索引:无论是快照读还是加锁读,都应尽可能利用索引。若当前读执行全表扫描(无合适索引),Next-Key Lock 会退化为锁定整张表的所有间隙与行,直接导致全表无法写入,大幅增加死锁概率。
  3. 需要强绝对一致性时的选择:如果业务逻辑要求完全与真实物理存储零偏差,优先在事务开始时即使用 SELECT ... FOR UPDATE 锁定目标范围,或直接调整隔离级别为串行化。
相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。