MySQL MVCC 原理解析:Undo Log、ReadView 与版本可见性机制

目录

[一、什么是 MVCC](#一、什么是 MVCC)

[二、MVCC 的底层实现依赖](#二、MVCC 的底层实现依赖)

[2.1 Undo Log(回滚日志)](#2.1 Undo Log(回滚日志))

[2.2 InnoDB 隐藏字段](#2.2 InnoDB 隐藏字段)

[2.3 ReadView(读视图)](#2.3 ReadView(读视图))

三、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_idReadView 进行比较。

主要有四种情况:

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_idReadView 进行比较。

主要判断逻辑:

复制代码
1 trx_id == 当前事务ID       → 可见
2 trx_id < min_trx_id        → 可见
3 trx_id ∈ 活跃事务列表       → 不可见
4 trx_id > max_trx_id        → 不可见

如果数据版本 可见,则直接返回该数据。

④ 不可见则查找历史版本

如果当前版本不可见:

  1. 通过 roll_pointer 找到 Undo Log

  2. 获取上一版本数据

  3. 再次进行 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;

执行流程:

  1. 记录旧版本到 Undo Log

  2. 标记当前记录为 删除状态

  3. 更新 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 UPDATESELECT ... LOCK IN SHARE MODEUPDATEDELETE 等。这类操作需要保证读取的数据是最新且可修改的,因此会通过 加锁机制(行锁) 来实现,而 不会使用 ReadView,也不会走 MVCC 的可见性判断

相关推荐
tsyjjOvO2 小时前
SpringMVC 从入门到精通(续)
java·后端·spring
bug远离Jemma2 小时前
MySql基本使用命令记录
数据库·mysql·oracle
Leon-Ning Liu2 小时前
SQL Server在ldf文件误删的情况下恢复数据库
数据库·sqlserver
木易 士心2 小时前
AI辅助开发:前端“加速器”还是后端“稳定器”?——基于项目类型与用户规模的实战指南
人工智能·后端
于先生吖2 小时前
基于 Java 开发短剧系统:完整架构与核心功能实现
java·开发语言·架构
专注_每天进步一点点2 小时前
mysql-connector-j(8.0 及以上版本,包括你使用的 8.3.0)并非采用 GPL 许可证,因此你在项目中引入该依赖时,不需要遵循 GPL 的开源要求(比如开源你的整个项目)
数据库·mysql·apache
WangYaolove13142 小时前
基于循环神经网络的情感分类(源码+文档)
python·mysql·django·毕业设计·源码
badhope2 小时前
GitHub超有用项目推荐:skill仓库--用技能树打造AI超频引擎
java·开发语言·前端·人工智能·python·重构·github
一只鹿鹿鹿2 小时前
网络安全风险评估报告如何写?(Word文件)
java·大数据·spring boot·安全·web安全·小程序