MVCC DEEP DIVE · 多版本并发控制源码级解析

MySQL InnoDB MVCC机制
Undo Log 与 Read View 的协作

InnoDB 的多版本并发控制(MVCC)是 MySQL 事务隔离的基石。本文通过源码分析 Undo Log 的版本链结构、Read View 的生成时机,以及 RR 与 RC 隔离级别下可见性判断算法的差异。

ISOLATION
RR (Default)
CORE STRUCT
Undo Chain
VIEW FIELDS
4 Fields
LOCK MODE
Non-locking Read
Current Record (最新行记录) Data trx_id=C roll_ptr ↓ Undo Log B (历史版本) Data trx_id=B roll_ptr ↓ Undo Log A (初始版本) Data trx_id=A roll_ptr=null Latest Row Undo Log VERSION CHAIN
PREFACE · 前言

在关系型数据库领域,锁机制是解决并发冲突的传统方案。悲观锁会阻塞读写,严重降低并发吞吐;而乐观锁依赖版本号实现冲突检测。InnoDB 采用 MVCC(多版本并发控制),实现了不加锁的一致性读,极大提升数据库并发能力,也是 MySQL 能够实现读不阻塞写、写不阻塞读的核心原理。

很多开发者只知道 MVCC 依靠 Undo Log 实现数据多版本,但不清楚Undo 版本链如何组织、Read View 如何控制数据可见性、RC 与 RR 隔离级别产生幻读差异的底层根源。本文从数据行存储结构出发,逐层拆解 MVCC 完整工作流程,厘清隔离级别底层实现差异,同时梳理线上开发中常见 MVCC 相关陷阱。

01
CHAPTER 01

InnoDB 行数据隐藏列基础

InnoDB 聚簇索引的每条记录,都会内置三个隐藏字段,是 MVCC 实现的基础:

DB_TRX_ID
6 bytes
事务ID。最近修改这条记录的事务 ID,新增、更新操作都会赋值;只读查询不会修改该字段。
DB_ROLL_PTR
7 bytes
回滚指针。指向这条记录对应的 Undo Log,用来串联不同历史版本,形成版本链。
DB_ROW_ID
6 bytes
隐藏主键。当表没有主键时,自动生成行 ID;存在主键时不会启用。
重点:更新记录不会直接覆盖原始数据,而是产生新版本,旧版本通过 roll_ptr 链接到 Undo Log。
02
CHAPTER 02

Undo Log:构建数据历史版本链

2.1 Undo Log 核心作用

Undo Log 称之为回滚日志,两大核心职责:

  • 事务回滚:事务失败时,依靠 Undo Log 恢复数据,保证原子性
  • MVCC 多版本读取:保存数据修改前的历史快照,供一致性读读取旧版本数据

Undo Log 分为两种类型:

I
insert undo log
插入事务产生,事务提交后可以直接回收。
U
update undo log
更新 / 删除事务产生,需要等待 MVCC 快照不再依赖之后才能清理。

2.2 版本链形成流程

我们模拟一条记录连续更新,演示版本链构建:

  1. 事务A插入数据,行数据 trx_id=A,roll_ptr 为空
  2. 事务B更新这条记录:生成新数据,新记录 trx_id=B;roll_ptr 指向事务A对应的 Undo
  3. 事务C再次更新:生成新版本,roll_ptr 指向事务B的 Undo 记录

最终形成单向链表:最新行记录 → UndoB → UndoA,这条链表就是版本链。当执行一致性读时,MySQL 沿着 roll_ptr 回溯版本链,筛选出当前事务可见的数据版本。

VERSION CHAIN · 历史版本链表结构
Current Record
TRX_C
data: "latest"
trx_id: C
roll_ptr: → UndoB
roll_ptr
指向历史版本
Undo Log B
TRX_B
data: "version_2"
trx_id: B
roll_ptr: → UndoA
roll_ptr
继续回溯
Undo Log A
TRX_A
data: "initial"
trx_id: A
roll_ptr: null
链表终点

2.3 清理机制:Purge 线程

Undo Log 会持续占用存储空间,InnoDB 后台 Purge 线程负责清理过期 Undo。如果业务存在长时间运行的事务,会持续持有旧数据快照,大量 Undo Log 无法被 Purge 回收,造成磁盘暴涨,这是线上经典故障场景。

03
CHAPTER 03

Read View:可见性判断的核心视图

Read View(读视图)是事务执行一致性读时,用来判断哪个版本数据对当前事务可见的判断依据。Read View 包含四个核心字段:

struct ReadView
m_low_limit_id
下一个即将分配的事务ID;所有大于等于该ID的事务,均不可见。
m_up_limit_id
当前活跃事务最小ID;小于该ID的事务均已提交可见。
m_creator_trx_id
创建该视图的事务ID。用于判断版本是否属于自身。
m_ids
创建视图瞬间,当前还未提交的活跃事务ID集合。

3.1 通用可见性判定规则

遍历版本链中每一条数据版本的 trx_id,按照以下逻辑判断:

1
trx_id < m_up_limit_id
✓ 事务已提交,版本可见
2
trx_id >= m_low_limit_id
3
m_up_limit_id <= trx_id < m_low_limit_id
✓ 不在 m_ids 中 → 已提交
4
若当前版本不可见
顺着 roll_ptr 跳转版本链上一条历史记录,继续判断。
04
CHAPTER 04

RC与RR隔离级别:Read View 创建时机差异

核心差异:Read View 创建时机,正是 READ COMMITTED(读已提交)和 REPEATABLE READ(可重复读)隔离级别行为差异的根源。

4.1 READ COMMITTED (RC 读已提交)

RC 每次 SELECT 均生成全新 Read View
BEGIN
RV_1
SELECT
RV_2
SELECT
特性:每次查询获取最新活跃事务快照,可以读到其他事务已经提交的数据。
缺陷:同一事务内多次查询,结果不一致,会产生不可重复读;不会避免幻读。

4.2 REPEATABLE READ (RR 可重复读,MySQL 默认)

RR 事务中第一次 SELECT 生成 Read View,全程复用
BEGIN
RV_1
SELECT
复用 RV_1
SELECT
特性:事务全程使用同一套可见性规则,保证多次读取结果一致,规避不可重复读。
MySQL InnoDB 在 RR 级别通过 MVCC + 间隙锁 共同解决幻读问题。
重要区分:单纯依靠 MVCC 只能解决快照读下的幻读;当前读(select ... for update)依靠锁机制防止幻读。
05
CHAPTER 05

快照读 vs 当前读(MVCC适用边界)

很多开发者混淆两种读取方式,导致对 MVCC 理解出现偏差:

S
快照读 (一致性非锁定读)
普通 SELECT 语句,不加锁,使用 MVCC 读取历史版本,不加锁、并发高。
SELECT * FROM user WHERE id = 1;
C
当前读 (锁定读)
读取数据最新版本,会加行锁,不使用 MVCC 快照。
SELECT ... FOR UPDATE;
SELECT ... LOCK IN SHARE MODE;
UPDATE / DELETE
关键知识点:MVCC 只服务于快照读;当前读直接读取最新数据,依赖行锁实现隔离。
06
CHAPTER 06

RR隔离级别下幻读深度答疑

标准 SQL 规范中,RR 允许幻读,但是 MySQL InnoDB 在 RR 级别消除了幻读。

  • 快照读:依靠 MVCC,读取事务启动时的快照,不会感知其他事务新增数据,天然不存在幻读
  • 当前读:依靠 Next-Key Lock(临键锁 = 记录锁 + 间隙锁)锁住区间,阻止其他事务插入新数据,杜绝幻读
07
CHAPTER 07

线上开发 MVCC 常见踩坑点

1
长事务导致 Undo 堆积、磁盘持续上涨
长时间运行的事务会持续持有旧 Read View,Purge 线程无法清理 Undo Log,ibdata1 文件持续膨胀。
优化方案:杜绝事务长时间空闲,拆分超大事务。
2
RC 隔离级别下,主从延迟引发数据不一致
RC 隔离级别无间隙锁,写入并发高时更容易产生锁冲突,同时 binlog 格式需要合理配置。互联网业务大量使用 RC 隔离级别,需要做好并发控制。
3
误以为 MVCC 完全不需要锁
MVCC 优化的是快照读;更新、删除、for update 当前读依旧需要行锁,高并发热点行依旧会出现锁等待。
4
不要依赖 MVCC 实现超长时间的数据快照
事务持续数小时会带来严重存储压力,历史版本链过长,查询需要遍历大量 Undo,SQL 性能急剧下降。
08
CHAPTER 08

结语

EPILOGUE · 结语

MVCC 整套机制由 Undo Log 版本链与 Read View 协同完成:Undo Log 保存数据所有历史版本,构成可回溯的版本链表;Read View 提供统一的可见性判断标准;同时 Read View 创建时机区分了 RC 与 RR 两大隔离级别。

理解 MVCC 底层原理,不仅仅是面试知识点,更能指导业务选型:什么时候选择 RC、什么时候使用 RR;如何规避长事务隐患;区分快照读与当前读,合理优化锁冲突。很多线上 MySQL 死锁、事务超时、磁盘暴涨问题,根源都是没有吃透 MVCC 与 InnoDB 锁机制的配合关系。