目录
[一、什么是 MVCC](#一、什么是 MVCC)
[二、MVCC 的底层实现依赖](#二、MVCC 的底层实现依赖)
[2.1 Undo Log(回滚日志)](#2.1 Undo Log(回滚日志))
[2.2 InnoDB 隐藏字段](#2.2 InnoDB 隐藏字段)
[2.3 ReadView(读视图)](#2.3 ReadView(读视图))
[3.1 ReadView 的创建时机](#3.1 ReadView 的创建时机)
[1️⃣ REPEATABLE READ(默认)](#1️⃣ REPEATABLE READ(默认))
[2️⃣ READ COMMITTED](#2️⃣ READ COMMITTED)
[3.2 ReadView 可见性规则](#3.2 ReadView 可见性规则)
[四、MVCC 的实际执行过程](#四、MVCC 的实际执行过程)
[4.1 查询操作(快照读)](#4.1 查询操作(快照读))
[① 创建 ReadView](#① 创建 ReadView)
[② 读取数据记录](#② 读取数据记录)
[③ 判断数据是否可见](#③ 判断数据是否可见)
[④ 不可见则查找历史版本](#④ 不可见则查找历史版本)
[4.2 更新操作(UPDATE)](#4.2 更新操作(UPDATE))
[① 记录旧版本数据](#① 记录旧版本数据)
[② 修改数据记录](#② 修改数据记录)
[③ 形成版本链](#③ 形成版本链)
[4.3 删除操作(DELETE)](#4.3 删除操作(DELETE))
[4.4 MVCC 执行流程总结](#4.4 MVCC 执行流程总结)
在 MySQL 的并发控制机制中,主要依赖 锁机制(Lock) 和 MVCC(Multi-Version Concurrency Control,多版本并发控制) 两种方式来保证数据的一致性与系统并发性能。
锁机制通过对数据资源加锁来控制事务之间的访问顺序,例如常见的 共享锁(S锁)、排他锁(X锁)以及意向锁(IS / IX) 等,从而避免多个事务同时修改同一数据而产生冲突。
关于 MySQL 锁机制的详细原理与分类,我在之前的文章 《MySQL 中锁的分类与加锁方式小结》 中已经进行了系统整理,这里就不再展开说明。
本文主要重点介绍 MySQL InnoDB 的 MVCC 实现原理。
一、什么是 MVCC
MVCC(Multi-Version Concurrency Control),中文叫 多版本并发控制。
它的核心思想其实非常简单:
为一条数据维护多个版本,使读操作可以读取历史版本的数据,从而减少锁冲突。
在传统的锁机制中:
读 → 需要加锁
写 → 需要加锁
如果并发量很大,就会出现大量 锁等待。
而 MVCC 采用另一种方式:
写操作 → 创建新版本
读操作 → 读取旧版本
这样就实现了:
-
读不阻塞写
-
写不阻塞读
从而大幅提升数据库的并发性能。
二、MVCC 的底层实现依赖
在 InnoDB 中,MVCC 的实现主要依赖 三个核心机制:
Undo Log
隐藏字段
ReadView
它们共同组成了 MVCC 的实现基础。
2.1 Undo Log(回滚日志)
Undo Log 用于记录数据修改前的旧版本数据。
当一条记录被修改时:
更新前数据 → 写入 Undo Log
更新后数据 → 写入数据页
这样就形成了 版本链:
最新版本
↓
旧版本
↓
更旧版本
如果某个事务需要读取旧版本数据,就可以通过 Undo Log 找到历史版本。
Undo Log 的作用主要包括:
1️⃣ 事务回滚
2️⃣ MVCC 读取历史版本
2.2 InnoDB 隐藏字段
InnoDB 在每一条记录后面都会自动维护 两个隐藏字段:
| 字段 | 作用 |
|---|---|
| trx_id | 创建该数据版本的事务ID |
| roll_pointer | 指向 Undo Log 中的旧版本 |
例如:
数据记录
├─ id
├─ name
├─ trx_id
└─ roll_pointer
含义:
-
trx_id:表示是哪一个事务修改了这条记录
-
roll_pointer:指向 Undo Log 中的历史版本
通过这两个字段,就可以形成 版本链:
当前版本
↓ roll_pointer
Undo Log(旧版本)
↓
更旧版本
2.3 ReadView(读视图)
MVCC 的核心其实是 ReadView。
ReadView 可以理解为:
一个用于判断数据版本是否可见的快照。
当执行查询时,InnoDB 会创建一个 ReadView,其中记录当前系统中的事务状态。
ReadView 中包含四个重要信息:
| 字段 | 含义 |
|---|---|
| min_trx_id | 当前系统中最小活跃事务ID |
| max_trx_id | 下一个将要分配的事务ID |
| trx_ids | 当前活跃事务列表 |
| creator_trx_id | 创建 ReadView 的事务ID |
这些信息用于判断:
某条记录的版本
是否对当前事务可见
三、ReadView细节解释
3.1 ReadView 的创建时机
ReadView 的创建与 事务隔离级别 有关。
1️⃣ REPEATABLE READ(默认)
第一次执行 SELECT 时创建 ReadView
事务开始
↓
第一次 SELECT
↓
创建 ReadView
↓
后续查询复用同一个 ReadView
因此整个事务期间看到的数据 始终一致。
2️⃣ READ COMMITTED
每次执行 SELECT 都会创建新的 ReadView。
SELECT
↓
创建 ReadView
SELECT
↓
重新创建 ReadView
所以每次查询都可能看到 最新提交的数据。
3.2 ReadView 可见性规则
当读取一条记录时,会根据 记录的 trx_id 与 ReadView 进行比较。
主要有四种情况:
1 当前事务修改的数据
trx_id == 当前事务ID
结果:
可见
原因:当前事务总是可以看到自己修改的数据。
2 早于 ReadView 的事务
trx_id < min_trx_id
结果:
可见
原因:说明该数据在 ReadView 创建之前已经提交。
3 活跃事务修改的数据
trx_id ∈ trx_ids
结果:
不可见
原因:因为这些事务还没有提交。
4 未来事务
trx_id > max_trx_id
结果:
不可见
原因:说明该版本是在 ReadView 创建之后才产生的。
四、MVCC 的实际执行过程
4.1 查询操作(快照读)
在 InnoDB 中,大多数普通的查询语句:
SELECT * FROM user WHERE id = 1;
属于 快照读(Snapshot Read)。
执行流程大致如下:
① 创建 ReadView
当事务第一次执行查询时,InnoDB 会创建一个 ReadView,记录当前数据库中事务的状态,例如:
-
当前活跃事务列表
-
最小事务 ID
-
下一个即将分配的事务 ID
这个 ReadView 相当于 当前事务看到的数据快照环境。
② 读取数据记录
InnoDB 会读取数据页中的记录,每条记录中包含两个重要隐藏字段:
trx_id
roll_pointer
其中:
-
trx_id:表示该数据版本是由哪个事务生成
-
roll_pointer:指向 Undo Log 中的旧版本
③ 判断数据是否可见
系统会将 记录的 trx_id 与 ReadView 进行比较。
主要判断逻辑:
1 trx_id == 当前事务ID → 可见
2 trx_id < min_trx_id → 可见
3 trx_id ∈ 活跃事务列表 → 不可见
4 trx_id > max_trx_id → 不可见
如果数据版本 可见,则直接返回该数据。
④ 不可见则查找历史版本
如果当前版本不可见:
-
通过 roll_pointer 找到 Undo Log
-
获取上一版本数据
-
再次进行 ReadView 可见性判断
这个过程会不断向历史版本回溯,直到找到 符合条件的版本。
形成的结构如下:
当前版本
↓
Undo Log 版本1
↓
Undo Log 版本2
↓
Undo Log 版本3
最终返回 对当前事务可见的版本。
4.2 更新操作(UPDATE)
当执行更新语句:
UPDATE user SET age = 20 WHERE id = 1;
InnoDB 的执行过程如下:
① 记录旧版本数据
在修改数据之前,系统会将 当前数据写入 Undo Log。
旧数据 → Undo Log
这样可以保证:
-
事务回滚
-
MVCC 查询旧版本
② 修改数据记录
然后更新数据页中的记录:
age = 20
同时更新两个隐藏字段:
trx_id = 当前事务ID
roll_pointer = 指向Undo Log
③ 形成版本链
更新完成后,数据会形成 版本链结构:
最新版本 (trx_id = T2)
↓
旧版本 (Undo Log, trx_id = T1)
↓
更旧版本
之后如果有其他事务查询数据,就可以通过 版本链找到合适的历史版本。
4.3 删除操作(DELETE)
删除操作其实也不会立刻删除数据,而是执行 逻辑删除。
执行:
DELETE FROM user WHERE id = 1;
执行流程:
-
记录旧版本到 Undo Log
-
标记当前记录为 删除状态
-
更新 trx_id
结构仍然保留在数据页中:
记录
├ id
├ name
├ deleted flag
├ trx_id
└ roll_pointer
真正的物理删除会在之后由 Purge 线程完成。
4.4 MVCC 执行流程总结
整个 MVCC 的执行过程可以总结为:
数据修改
↓
生成 Undo Log
↓
形成版本链
↓
查询时创建 ReadView
↓
根据 ReadView 判断版本可见性
↓
如果不可见则沿 Undo Log 查找历史版本
最终实现:
读操作 → 读取历史版本
写操作 → 生成新版本
从而达到:
读不阻塞写
写不阻塞读
这也是 InnoDB 在高并发场景下能够保持良好性能的重要原因。
五、总结
ReadView 只在快照读(普通 SELECT)时创建,其创建时机取决于事务隔离级别:在 REPEATABLE READ 下事务第一次 SELECT 创建一次,在 READ COMMITTED 下每次 SELECT 都会重新创建。
需要注意的是,ReadView 只用于 快照读(Snapshot Read) 。所谓快照读,是指普通的 SELECT 查询语句,它不会对数据加锁,而是通过 MVCC + ReadView 判断当前事务可以看到哪个数据版本,从而读取历史版本数据。
与之相对应的是 当前读(Current Read) ,当前读指的是必须读取 最新版本数据 的操作,例如 SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE 等。这类操作需要保证读取的数据是最新且可修改的,因此会通过 加锁机制(行锁) 来实现,而 不会使用 ReadView,也不会走 MVCC 的可见性判断。