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 的可见性判断

相关推荐
XDHCOM17 小时前
ORA-32484重复列名错误,ORACLE数据库CYCLE子句故障修复与远程处理方案
数据库·oracle
涡能增压发动积17 小时前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
云烟成雨TD17 小时前
Spring AI Alibaba 1.x 系列【6】ReactAgent 同步执行 & 流式执行
java·人工智能·spring
Wenweno0o17 小时前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
于慨17 小时前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
swg32132117 小时前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
翻斗包菜17 小时前
PostgreSQL 日常维护完全指南:从基础操作到高级运维
运维·数据库·postgresql
tyung17 小时前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald17 小时前
SpringBoot - 自动配置原理
java·spring boot·后端
殷紫川17 小时前
深入理解 AQS:从架构到实现,解锁 Java 并发编程的核心密钥
java