MySQL MVCC 原理解析:Undo Log、ReadView 与版本可见性机制
目录
一、什么是 MVCC
二、MVCC 的底层实现依赖
2.1 Undo Log(回滚日志)
2.2 InnoDB 隐藏字段
2.3 ReadView(读视图)
三、ReadView细节解释
3.1 ReadView 的创建时机
1️⃣ REPEATABLE READ(默认)
2️⃣ READ COMMITTED
3.2 ReadView 可见性规则
四、MVCC 的实际执行过程
4.1 查询操作(快照读)
① 创建 ReadView
② 读取数据记录
③ 判断数据是否可见
④ 不可见则查找历史版本
4.2 更新操作(UPDATE)
① 记录旧版本数据
② 修改数据记录
③ 形成版本链
4.3 删除操作(DELETE)
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 的可见性判断。
