MySQL MVCC
面试频率:★★★★★
工作频率:★★★★☆
⚡30 秒速记
🟢 一句话
MVCC 是多版本并发控制,通过保存数据的历史版本,让不同事务根据可见性规则读取合适的数据版本,从而提高并发能力。
核心:
MVCC
│
├── 版本链
│ ↓
│ 历史版本
│
└── Read View
↓
判断哪个版本可见
重点记:
undo log
↓
历史版本
↓
版本链
↓
Read View判断可见性
↓
返回合适的数据版本
MVCC 主要用于:
快照读
例如普通:
SELECT * FROM user WHERE id = 1;
而:
SELECT * FROM user WHERE id = 1 FOR UPDATE;
属于:
当前读
需要读取符合条件的当前最新版本,并进行相应锁定。
一、什么是 MVCC?
MVCC:
Multi-Version Concurrency Control
中文:
多版本并发控制。
名字看起来复杂。
简单理解:
同一条数据可以存在多个历史版本,不同事务根据自己的可见性规则读取合适的数据版本。
例如:
数据库最新数据:
张三余额 = 500
但是某个事务:
可能仍然读取到:
张三余额 = 1000
因为:
1000
可能是这条数据的:
历史版本
二、为什么需要 MVCC? ⭐⭐⭐⭐⭐
假设:
张三余额 = 1000
事务A:
SELECT balance
FROM account
WHERE name = '张三';
第一次:
1000
这时候事务B:
UPDATE account
SET balance = 500
WHERE name = '张三';
然后:
COMMIT
现在数据库最新数据:
张三余额 = 500
但是事务A:
在 REPEATABLE READ 下进行相应的快照读时,再次查询:
SELECT balance
FROM account
WHERE name = '张三';
可能仍然看到:
1000
问题:
数据库最新值明明已经是500
↓
为什么事务A还能读取1000?
答案:
MVCC
三、MVCC 最核心解决什么问题?
如果我们希望:
事务A读取数据期间
↓
数据不要被别人修改
最简单粗暴的方式:
加锁
例如:
事务A读取
↓
锁住数据
↓
事务B想修改
↓
等待
↓
事务A结束
↓
事务B继续
问题:
并发能力下降
MVCC 的思路:
事务A
↓
想看旧版本
↓
给A看旧版本
事务B
↓
修改数据
↓
产生新版本
这样很多情况下:
读不阻塞写
写不阻塞普通快照读
所以:
MVCC 的核心价值之一就是提高数据库并发能力。
四、历史版本从哪里来? ⭐⭐⭐⭐⭐
你前面已经学过:
undo log
undo log:
不仅可以用于:
rollback
还和:
MVCC历史版本
密切相关。
例如原来:
张三余额 = 1000
事务B修改:
1000 → 500
InnoDB 会维护恢复旧版本所需要的信息。
可以粗略理解:
当前版本
张三 = 500
↓
undo
↓
历史版本
张三 = 1000
如果以前还修改过:
800 → 1000 → 500
那么可以粗略理解成:
当前版本:
500
↓
历史版本:
1000
↓
更老版本:
800
↓
...
这就形成了:
版本链。
五、什么是版本链? ⭐⭐⭐⭐⭐
同一条记录:
可能存在多个版本。
例如:
最新:
张三 = 500
↓
张三 = 1000
↓
张三 = 800
↓
更老版本...
这些版本:
按照相关信息串起来。
可以理解成:
版本链
六、版本链怎么串起来?
InnoDB 记录中有一些隐藏信息。
今天重点认识两个:
trx_id
roll_pointer
七、trx_id 是什么?
可以简单理解:
这个版本最近是被哪个事务修改的。
例如:
事务20:
把张三:
1000 → 500
那么当前版本可以粗略理解:
张三 = 500
trx_id = 20
之前版本:
张三 = 1000
trx_id = 10
所以:
trx_id
↓
记录版本对应的事务信息
八、roll_pointer 是什么?
可以简单理解:
指向 undo 中对应的历史版本信息。
例如:
当前版本
张三 = 500
trx_id = 20
↓
roll_pointer
↓
历史版本
张三 = 1000
trx_id = 10
↓
roll_pointer
↓
更老版本
张三 = 800
trx_id = 5
所以:
trx_id
+
roll_pointer
+
undo log
共同帮助构成:
版本链
九、有了版本链,怎么知道该读哪个版本? ⭐⭐⭐⭐⭐
现在数据库有:
500
↓
1000
↓
800
事务A到底应该读取:
500?
1000?
800?
不能随便选。
所以需要:
Read View
十、什么是 Read View? ⭐⭐⭐⭐⭐
简单理解:
Read View 是 MVCC 判断某个数据版本对当前事务是否可见的一套规则 / 快照信息。
例如:
事务A查询数据
↓
找到最新版本500
↓
根据Read View判断
↓
500对事务A可见吗?
↓
不符合可见性规则
↓
沿版本链继续找
↓
找到1000
↓
判断
↓
可见
↓
返回1000
所以:
版本链
↓
提供多个版本
Read View
↓
决定看哪个版本
十一、MVCC 核心流程 ⭐⭐⭐⭐⭐
例如:
数据库版本链:
500
↓
1000
↓
800
事务A查询:
SELECT
↓
快照读
↓
获取 / 使用Read View
↓
检查当前版本
↓
版本可见?
↓ ↓
是 否
↓ ↓
返回 沿版本链找
↓
再判断
↓
找到可见版本
↓
返回
所以 MVCC:
可以简单记成:
版本链
+
Read View
=
决定事务看到哪个版本
十二、为什么数据库已经是500,事务A还能看到1000? ⭐⭐⭐⭐⭐
这是理解 MVCC 最关键的问题。
假设:
事务A第一次:
张三 = 1000
事务B:
1000 → 500
↓
commit
数据库最新版本:
500
事务A再次快照读:
最新版本500
↓
根据Read View判断
↓
这个版本不符合A的可见性规则
↓
沿版本链找
↓
找到1000
↓
符合可见性规则
↓
返回1000
所以:
数据库最新值
≠
当前事务一定看到的值
这是 MVCC 非常核心的思想。
十三、MVCC 和 REPEATABLE READ ⭐⭐⭐⭐⭐
你前面学过:
REPEATABLE READ
=
可重复读
例如:
事务A:
第一次查询
1000
事务B:
修改成500
commit
事务A:
再次快照读
仍可能1000
现在可以解释原因:
REPEATABLE READ
↓
MVCC
↓
Read View
↓
版本链
↓
找到符合可见性规则的历史版本
↓
1000
所以:
REPEATABLE READ
和:
MVCC
关系非常密切。
十四、READ COMMITTED 也使用 MVCC ⭐⭐⭐⭐⭐
不要误以为:
只有REPEATABLE READ使用MVCC
不是。
MySQL InnoDB 的:
READ COMMITTED
也会使用 MVCC。
它们一个重要区别在于:
Read View 的生成 / 使用时机不同。
十五、RC 和 RR 的 Read View 区别 ⭐⭐⭐⭐⭐
这是非常高频的面试点。
READ COMMITTED
可以简单理解:
每次快照读通常会创建新的 Read View。
例如:
事务A第一次查询
↓
Read View 1
↓
1000
事务B修改
1000 → 500
commit
事务A第二次查询
↓
新的Read View 2
↓
500
所以:
第一次:1000
第二次:500
这就是:
READ COMMITTED
下可能发生:
不可重复读
十六、REPEATABLE READ
REPEATABLE READ 下:
一个事务通常会复用首次快照读建立的 Read View。
例如:
事务A第一次快照读
↓
创建Read View
↓
1000
事务B修改
1000 → 500
commit
事务A再次快照读
↓
继续使用之前的Read View
↓
根据版本链
↓
仍然读取1000
所以:
第一次:1000
第二次:1000
实现:
可重复读
十七、RC 和 RR 一张图记住 ⭐⭐⭐⭐⭐
READ COMMITTED
第一次快照读
↓
Read View 1
↓
1000
事务B修改并提交
↓
第二次快照读
↓
Read View 2
↓
500
记:
RC
=
每次快照读通常重新生成Read View
REPEATABLE READ
第一次快照读
↓
Read View 1
↓
1000
事务B修改并提交
↓
第二次快照读
↓
继续使用Read View 1
↓
1000
记:
RR
=
事务中的快照读通常复用首次快照读建立的Read View
十八、什么是快照读? ⭐⭐⭐⭐⭐
普通 SELECT:
SELECT *
FROM user
WHERE id = 1;
在没有显式锁定等特殊情况时:
通常属于:
快照读
快照读:
主要通过:
MVCC
读取符合当前 Read View 可见性规则的数据版本。
所以:
普通SELECT
↓
快照读
↓
MVCC
↓
Read View
↓
版本链
十九、什么是当前读? ⭐⭐⭐⭐⭐
例如:
SELECT *
FROM user
WHERE id = 1
FOR UPDATE;
属于:
当前读
当前读:
需要读取符合条件的:
当前最新版本。
并且:
进行相应加锁
常见当前读操作包括:
SELECT ... FOR UPDATE
UPDATE
DELETE
以及其他需要读取当前最新数据并进行锁定的操作。
二十、快照读 vs 当前读 ⭐⭐⭐⭐⭐
快照读
例如:
SELECT *
FROM user
WHERE id = 1;
核心:
MVCC
↓
Read View
↓
读取合适版本
特点:
普通读取
并发能力高
当前读
例如:
SELECT *
FROM user
WHERE id = 1
FOR UPDATE;
核心:
读取当前最新版本
+
加锁
速记:
普通SELECT
↓
快照读
↓
MVCC
SELECT ... FOR UPDATE
↓
当前读
↓
最新版本 + 锁
二十一、为什么 RR 不是永远看到旧数据?
这个非常容易理解错。
假设:
事务A:
SELECT balance
FROM account
WHERE id = 1;
快照读:
1000
事务B:
修改成500
commit
事务A:
再次普通 SELECT:
可能仍然1000
但是如果事务A执行:
SELECT balance
FROM account
WHERE id = 1
FOR UPDATE;
这是:
当前读
它需要:
读取当前最新版本
所以不能简单说:
REPEATABLE READ
=
整个事务永远只能看到第一次的数据
更准确:
MVCC 主要影响快照读;当前读需要读取当前最新版本,并结合锁机制工作。
二十二、MVCC 为什么能提高并发性能? ⭐⭐⭐⭐⭐
传统思路:
读数据
↓
加锁
↓
别人修改
↓
等待
MVCC:
事务A读取
↓
读取适合自己的版本
事务B修改
↓
生成新版本
所以很多情况下:
读和写可以并发
避免:
普通SELECT全部通过互斥锁等待
因此:
MVCC 通过多版本机制减少读写之间的冲突,提高数据库并发能力。
二十三、MVCC 和 undo log 的关系 ⭐⭐⭐⭐⭐
undo log:
事务修改数据
↓
保存旧版本相关信息
↓
形成历史版本
↓
版本链
MVCC:
快照读
↓
Read View
↓
判断当前版本是否可见
↓
不可见
↓
沿版本链寻找历史版本
所以:
undo log
↓
历史版本
↓
版本链
↓
MVCC
二十四、MVCC 和 rollback 的关系
undo log 同时服务于:
事务回滚
以及:
MVCC历史版本
可以记:
undo log
│
┌───────┴───────┐
↓ ↓
rollback MVCC
↓ ↓
恢复数据 历史版本
所以昨天学的:
undo log
和今天:
MVCC
其实是连着的。
二十五、完整知识链 ⭐⭐⭐⭐⭐
现在可以把之前的知识全部串起来:
事务隔离级别
↓
REPEATABLE READ
↓
为什么可以重复读取?
↓
MVCC
↓
┌───────────────┐
↓ ↓
版本链 Read View
↓ ↓
历史版本 可见性判断
↓ ↓
└───────┬───────┘
↓
快照读
版本链又来自:
数据修改
↓
undo log
↓
历史版本
↓
版本链
所以完整:
undo log
↓
历史版本
↓
版本链
↓
MVCC
↓
Read View
↓
快照读
↓
REPEATABLE READ
🎤 面试回答(30秒)
MVCC 是多版本并发控制,主要目的是在保证一定事务隔离性的同时提高数据库并发能力。
InnoDB 在数据修改时会通过 undo log 等机制维护历史版本,并通过版本链保存记录的不同版本。
事务进行快照读时,会根据 Read View 判断当前数据版本是否可见,如果不可见,就沿着版本链寻找符合可见性规则的历史版本。
READ COMMITTED 和 REPEATABLE READ 都会使用 MVCC,一个重要区别是 RC 的快照读通常每次会创建新的 Read View,而 RR 通常会复用事务中首次快照读建立的 Read View,因此可以实现可重复读。
🎯 面试追问
Q1:什么是 MVCC?
答:
多版本并发控制
通过:
历史版本
+
Read View
让不同事务读取合适的数据版本。
Q2:MVCC 有什么作用?
答:
减少读写冲突
↓
提高数据库并发能力
Q3:MVCC 最核心的两个东西是什么?
答:
版本链
+
Read View
Q4:历史版本从哪里来?
答:
和:
undo log
密切相关。
数据修改后:
旧版本相关信息可以通过 undo 维护。
Q5:trx_id 是什么?
答:
可以简单理解:
记录这个版本最近由哪个事务修改。
Q6:roll_pointer 是什么?
答:
可以简单理解:
指向 undo 中的历史版本信息,用于沿版本链寻找旧版本。
Q7:Read View 是什么?
答:
MVCC 用于判断某个数据版本对当前事务是否可见的一套快照信息和判断规则。
Q8:为什么数据库最新是500,事务还能读取1000?
答:
因为:
500版本
↓
Read View判断不可见
↓
沿版本链寻找
↓
找到1000
↓
1000可见
↓
返回1000
Q9:READ COMMITTED 和 REPEATABLE READ 都使用 MVCC 吗?
答:
是
一个重要区别:
RC
↓
每次快照读通常生成新的Read View
RR
↓
通常复用事务首次快照读建立的Read View
Q10:什么是快照读?
答:
普通 SELECT 通常属于快照读。
例如:
SELECT *
FROM user
WHERE id = 1;
主要通过:
MVCC
读取合适的数据版本。
Q11:什么是当前读?
答:
需要读取符合条件的:
当前最新版本
并进行相应锁定。
例如:
SELECT *
FROM user
WHERE id = 1
FOR UPDATE;
Q12:快照读和当前读有什么区别?
答:
快照读
↓
MVCC
↓
读取符合Read View的数据版本
当前读
↓
读取当前最新版本
↓
加锁
Q13:MVCC 是不是完全不用锁?
答:
不是。
MVCC:
主要解决:
快照读
的并发读取问题。
数据库中的:
UPDATE
DELETE
SELECT ... FOR UPDATE
等当前读 / 写操作:
仍然会涉及锁。
Q14:undo log 和 MVCC 有什么关系?
答:
undo log
↓
维护历史版本相关信息
↓
形成版本链
↓
MVCC可以寻找历史版本
⚠️ 工作 / 面试易错点
1. 认为 MVCC 就是不加锁
错误。
MVCC
主要用于快照读
而:
UPDATE
DELETE
SELECT ... FOR UPDATE
仍然涉及:
锁
2. 认为只有 RR 使用 MVCC
错误。
READ COMMITTED
和
REPEATABLE READ
都可以使用 MVCC。
主要区别之一:
Read View的生成 / 使用时机不同
3. 认为 RR 整个事务永远看到同一个值
不准确。
要区分:
快照读
vs
当前读
普通 SELECT:
快照读
而:
SELECT ... FOR UPDATE
属于:
当前读
4. 把 undo log 只理解成 rollback
不完整。
undo log:
rollback
+
MVCC历史版本
都非常重要。
❓ 自测
- 什么是 MVCC?
- 为什么需要 MVCC?
- MVCC 为什么能提高数据库并发能力?
- MVCC 最核心的两个东西是什么?
- 什么是版本链?
- 历史版本和 undo log 有什么关系?
- trx_id 是什么?
- roll_pointer 是什么?00
- Read View 是什么?
- Read View 有什么作用?
- 为什么数据库最新值是500,事务还能读取1000?
- READ COMMITTED 使用 MVCC 吗?
- REPEATABLE READ 使用 MVCC 吗?
- RC 和 RR 在 Read View 上有什么重要区别?
- 什么是快照读?
- 普通 SELECT 通常属于什么读?
- 什么是当前读?
- SELECT ... FOR UPDATE 属于什么读?
- 快照读和当前读有什么区别?
- MVCC 是不是完全不需要锁?
- undo log 和 rollback 有什么关系?
- undo log 和 MVCC 有什么关系?
- REPEATABLE READ 为什么可以实现可重复读?
Read View 可见性判断 ⭐⭐⭐⭐⭐
Read View 是 MVCC 的核心,用来判断版本链中的某个数据版本,对当前事务是否可见。
⚡30 秒速记
Read View 重点记 4 个:
m_ids
= 创建 Read View 时,活跃未提交的事务ID集合
min_trx_id
= m_ids 中最小的事务ID
max_trx_id
= 创建 Read View 时,下一个将要分配的事务ID
creator_trx_id
= 创建这个 Read View 的事务自己的ID
判断某个数据版本能不能看:
版本的 trx_id
↓
是不是自己修改的?
↓
是 → ✅ 可见
↓
否
↓
trx_id < min_trx_id?
↓
是 → ✅ 可见
↓
否
↓
trx_id >= max_trx_id?
↓
是 → ❌ 不可见
↓
否
↓
trx_id 在 m_ids 中?
↙ ↘
是 否
↓ ↓
❌不可见 ✅可见
最简单的记忆方式:
自己改的
→ 看
很早以前已经提交的
→ 看
Read View之后才出现的
→ 不看
中间的事务
→ 看m_ids
还活着 → 不看
已经提交 → 看
一、为什么需要 Read View? ⭐⭐⭐⭐⭐
假设一条数据存在多个版本:
当前版本:
张三余额 = 500
trx_id = 20
↓ roll_pointer
历史版本:
张三余额 = 1000
trx_id = 10
↓ roll_pointer
更老版本:
张三余额 = 800
trx_id = 5
现在事务30执行:
SELECT balance
FROM account
WHERE name = '张三';
到底应该返回:
500?
1000?
800?
不能随便返回。
需要:
Read View
判断:
哪个版本对当前事务可见。
所以:
版本链
= 提供有哪些版本
Read View
= 决定能看哪个版本
二、Read View 是什么? ⭐⭐⭐⭐⭐
可以简单理解:
Read View 是事务进行快照读时,用来判断数据版本可见性的一套快照信息和规则。
流程:
SELECT
↓
快照读
↓
使用Read View
↓
查看当前版本
↓
判断trx_id
↓
当前版本可见?
↙ ↘
是 否
↓ ↓
返回 沿版本链继续找
↓
找历史版本
↓
再判断可见性
三、Read View 四个核心 ⭐⭐⭐⭐⭐
重点:
m_ids
min_trx_id
max_trx_id
creator_trx_id
四、m_ids 是什么?
假设创建 Read View 时:
事务20:正在执行
事务30:正在执行
事务40:正在执行
那么可以粗略理解:
m_ids = [20, 30, 40]
表示:
创建 Read View 时,当前活跃、还没有提交的事务 ID 集合。
所以:
某个版本的trx_id
如果在m_ids里面
↓
说明创建Read View的时候
这个事务还没提交
因此:
通常不可见
五、min_trx_id 是什么?
就是:
m_ids中最小的事务ID
例如:
m_ids = [20, 30, 40]
那么:
min_trx_id = 20
如果某个版本:
trx_id = 10
那么:
10 < 20
说明:
修改这个版本的事务,在创建 Read View 之前已经提交完成。
所以:
✅ 可见
六、max_trx_id 是什么? ⭐⭐⭐⭐⭐
可以简单理解:
创建 Read View 时,下一个将要分配的事务 ID。
例如:
当前事务 ID 已经分配到:
40
那么:
max_trx_id = 41
注意:
max_trx_id
不是:
m_ids里面最大的事务ID
例如:
m_ids = [20, 30, 40]
max_trx_id = 41
这是非常容易记错的地方。
如果某个版本:
trx_id = 50
而:
max_trx_id = 41
满足:
50 >= 41
说明:
修改这个版本的事务,是在当前 Read View 创建之后才出现的。
所以:
❌ 不可见
七、creator_trx_id 是什么?
表示:
创建当前 Read View 的事务自己的 ID。
例如:
事务30创建Read View
那么:
creator_trx_id = 30
如果数据版本:
trx_id = 30
说明:
这个数据就是当前事务自己修改的
所以:
✅ 可见
八、为什么自己修改的数据必须可见?
例如:
事务30:
UPDATE account
SET balance = 500
WHERE id = 1;
然后自己执行:
SELECT balance
FROM account
WHERE id = 1;
当然应该看到:
500
不能:
我自己刚改成500
↓
自己查询
↓
还给我1000
所以:
trx_id == creator_trx_id
↓
自己修改的数据
↓
✅ 可见
九、可见性规则1:自己修改的数据 ⭐⭐⭐⭐⭐
判断:
trx_id == creator_trx_id
例如:
数据版本:
trx_id = 30
Read View:
creator_trx_id = 30
说明:
当前事务自己修改的
结果:
✅ 可见
十、可见性规则2:很早以前已经提交 ⭐⭐⭐⭐⭐
判断:
trx_id < min_trx_id
例如:
trx_id = 10
min_trx_id = 20
满足:
10 < 20
说明:
修改这个版本的事务
↓
在创建Read View之前
↓
已经提交完成
所以:
✅ 可见
记:
比最小活跃事务还小
↓
老事务
↓
已经结束
↓
可以看
十一、可见性规则3:Read View之后出现 ⭐⭐⭐⭐⭐
判断:
trx_id >= max_trx_id
例如:
trx_id = 50
max_trx_id = 41
说明:
事务50
↓
创建Read View的时候
↓
还没有出现
所以:
❌ 不可见
记:
Read View都拍完照片了
↓
你才出现
↓
当然不能出现在这张照片里
十二、可见性规则4:处于中间范围 ⭐⭐⭐⭐⭐
这是最容易绕的。
假设:
min_trx_id = 20
max_trx_id = 41
现在:
trx_id = 25
满足:
20 <= 25 < 41
这个时候:
不能直接判断。
要继续看:
25在不在m_ids里面?
情况1:在 m_ids
例如:
m_ids = [20, 25, 30, 40]
里面有:
25
说明:
创建Read View的时候
↓
事务25还活着
↓
还没有提交
所以:
❌ 不可见
情况2:不在 m_ids
例如:
m_ids = [20, 30, 40]
里面:
没有25
说明:
事务25虽然ID处于这个范围
↓
但是创建Read View的时候
↓
它已经提交了
所以:
✅ 可见
十三、为什么中间范围还要查 m_ids?
假设:
事务20:执行很慢,还没提交
事务21:开始
↓
事务21很快执行完
↓
commit
事务22:开始
此时:
事务20
可能还活着。
但是:
事务21
已经提交。
所以事务 ID:
大
不代表:
一定还没提交
因此:
20 <= trx_id < max_trx_id
这种情况:
必须查看:
m_ids
才能知道这个事务创建 Read View 时:
到底有没有提交
十四、完整判断流程 ⭐⭐⭐⭐⭐
以后面试就按照这个顺序说:
拿到数据版本 trx_id
↓
trx_id == creator_trx_id?
↓
是
↓
自己修改
↓
✅ 可见
否则:
trx_id < min_trx_id?
↓
是
↓
Read View创建前已经提交
↓
✅ 可见
否则:
trx_id >= max_trx_id?
↓
是
↓
Read View创建后才出现
↓
❌ 不可见
否则:
min_trx_id <= trx_id < max_trx_id
↓
检查m_ids
↓
┌────┴────┐
↓ ↓
在 不在
↓ ↓
还没提交 已经提交
↓ ↓
❌不可见 ✅可见
十五、完整案例 ⭐⭐⭐⭐⭐
假设 Read View:
m_ids = [20, 30]
min_trx_id = 20
max_trx_id = 31
creator_trx_id = 30
现在版本链:
当前版本:
张三余额 = 500
trx_id = 20
↓
历史版本:
张三余额 = 1000
trx_id = 10
事务30执行:
SELECT balance
FROM account
WHERE name = '张三';
第一步:看500
500
trx_id = 20
判断:
是不是自己?
20 != 30
↓
不是
继续:
20 < min_trx_id(20)?
↓
不是
继续:
20 >= max_trx_id(31)?
↓
不是
继续:
20在m_ids里面吗?
有:
m_ids = [20,30]
所以:
在
说明:
创建Read View的时候
事务20还没有提交
因此:
500 ❌ 不可见
十六、当前版本不可见怎么办?
沿:
roll_pointer
寻找历史版本:
500
trx_id = 20
↓
roll_pointer
↓
1000
trx_id = 10
现在判断:
trx_id = 10
而:
min_trx_id = 20
满足:
10 < 20
说明:
事务10在Read View创建前
已经提交
所以:
1000 ✅ 可见
最终:
SELECT balance ...
返回:
1000
十七、这就是 MVCC 的完整过程 ⭐⭐⭐⭐⭐
现在可以把 MVCC 真正串起来:
SELECT
↓
快照读
↓
Read View
↓
查看当前数据版本
↓
trx_id
↓
判断是否可见
↓
不可见
↓
roll_pointer
↓
undo log
↓
历史版本
↓
再次判断
↓
找到可见版本
↓
返回
所以:
MVCC
=
版本链
+
Read View
+
可见性判断
十八、READ COMMITTED 为什么会不可重复读? ⭐⭐⭐⭐⭐
现在可以从 Read View 真正理解:
READ COMMITTED:
每次快照读通常都会创建新的 Read View。
例如:
事务A:
第一次查询
此时:
事务20还没提交
Read View:
m_ids = [20]
事务20修改的数据:
不可见
所以:
第一次:
1000
然后事务20:
commit
事务A:
再次查询。
READ COMMITTED:
重新创建Read View
新的:
m_ids = []
事务20:
已经提交。
所以:
500变成可见
结果:
第一次:
1000
第二次:
500
所以:
READ COMMITTED
↓
可能发生不可重复读
十九、REPEATABLE READ 为什么可以重复读? ⭐⭐⭐⭐⭐
REPEATABLE READ:
同一个事务中的快照读通常复用首次快照读建立的 Read View。
例如:
事务A第一次:
创建Read View
当时:
事务20还没提交
所以:
m_ids包含20
第一次:
500不可见
↓
读取1000
然后:
事务20:
commit
事务A再次快照读。
仍然使用:
之前的Read View
在这个 Read View 的可见性规则下:
事务20修改的500
仍然不可见
于是沿版本链:
500
↓
1000
最终:
还是1000
所以:
第一次:
1000
第二次:
1000
这就是:
REPEATABLE READ
↓
可重复读
二十、RC 和 RR 最核心区别 ⭐⭐⭐⭐⭐
READ COMMITTED
第一次快照读
↓
Read View A
↓
1000
别人提交500
第二次快照读
↓
新的Read View B
↓
500
所以:
每次快照读通常创建新的Read View
REPEATABLE READ
第一次快照读
↓
Read View A
↓
1000
别人提交500
第二次快照读
↓
继续使用Read View A
↓
1000
所以:
事务中的快照读通常复用首次快照读的Read View
二十一、用拍照理解 Read View ⭐⭐⭐⭐⭐
这是最好记的方法。
把:
Read View
理解成:
给数据库当前事务状态拍了一张照片。
例如:
拍照的时候:
事务20:没提交
事务30:自己
事务40:没提交
所以照片:
m_ids = [20,30,40]
以后判断:
这个数据版本
在我拍照片的时候
应该存在吗?
如果:
很早以前已经提交
当然:
照片里应该看得到
如果:
拍完照片以后才出现
当然:
看不到
如果:
拍照的时候事务还没提交
也:
看不到它修改的数据
这就是 Read View。
二十二、四个变量怎么记? ⭐⭐⭐⭐⭐
不要死背英文。
直接记:
m_ids
=
拍照时还有谁没干完?
min_trx_id
=
这些没干完的人里面
ID最小的是谁?
max_trx_id
=
接下来准备分配到哪个事务ID?
creator_trx_id
=
拍照片的人是谁?
二十三、Read View 和版本链的关系 ⭐⭐⭐⭐⭐
版本链负责:
有什么版本?
例如:
500
↓
1000
↓
800
Read View负责:
哪个版本我能看?
所以:
版本链
= 菜单
Read View
= 筛选规则
最终:
版本链
+
Read View
↓
找到当前事务可见的数据
二十四、Read View 和当前读
注意:
Read View 主要和:
快照读
有关。
例如:
SELECT *
FROM user
WHERE id = 1;
通常:
快照读
↓
MVCC
↓
Read View
但是:
SELECT *
FROM user
WHERE id = 1
FOR UPDATE;
属于:
当前读
需要:
读取当前最新版本
+
加锁
所以:
不要把 Read View 的历史版本读取逻辑直接套到所有 SQL 上。
二十五、最终知识链 ⭐⭐⭐⭐⭐
现在整个 MVCC 已经可以串起来:
数据被UPDATE
↓
undo log维护历史版本相关信息
↓
形成版本链
↓
当前版本
trx_id = 20
↓
历史版本
trx_id = 10
↓
更老版本
查询:
普通SELECT
↓
快照读
↓
MVCC
↓
Read View
↓
判断trx_id是否可见
↓
不可见?
↓
沿版本链继续找
↓
找到可见版本
↓
返回
最终:
MVCC
│
├── undo log
│ ↓
│ 历史版本
│
├── 版本链
│ ↓
│ trx_id
│ roll_pointer
│
└── Read View
↓
可见性判断
🎤 面试回答(30秒)
Read View 是 InnoDB MVCC 中用于判断数据版本可见性的机制。
它主要包含活跃事务集合 m_ids、最小活跃事务 ID min_trx_id、下一个待分配事务 ID max_trx_id,以及创建 Read View 的事务 ID creator_trx_id。
进行快照读时,InnoDB 会根据数据版本的 trx_id 和 Read View 判断当前版本是否可见。
如果当前版本不可见,就会结合 undo log 和版本链继续寻找符合可见性规则的历史版本。
READ COMMITTED 通常每次快照读创建新的 Read View,而 REPEATABLE READ 通常复用事务首次快照读建立的 Read View,这也是 RR 能实现可重复读的重要原因。
🎯 面试追问
Q1:Read View 是什么?
答:
MVCC 中用于判断某个数据版本对当前事务是否可见的一套快照信息和判断规则。
Q2:Read View 有哪几个核心字段?
答:
m_ids
min_trx_id
max_trx_id
creator_trx_id
Q3:m_ids 是什么?
答:
创建Read View时
活跃、未提交的事务ID集合
Q4:min_trx_id 是什么?
答:
m_ids中最小的事务ID
Q5:max_trx_id 是 m_ids 最大值吗?
答:
不是
可以简单理解为:
创建Read View时
下一个将要分配的事务ID
Q6:creator_trx_id 是什么?
答:
创建当前Read View的事务自己的ID
Q7:trx_id < min_trx_id 说明什么?
答:
说明修改这个版本的事务:
在Read View创建之前
已经提交
所以:
✅ 可见
Q8:trx_id >= max_trx_id 说明什么?
答:
说明:
修改这个版本的事务
在Read View创建之后才出现
所以:
❌ 不可见
Q9:trx_id 在 m_ids 里说明什么?
答:
说明创建 Read View 时:
这个事务还没有提交
所以它修改的版本:
❌ 不可见
Q10:trx_id 不在 m_ids 里说明什么?
如果:
min_trx_id <= trx_id < max_trx_id
同时:
trx_id不在m_ids
说明:
创建Read View时
这个事务已经提交
所以:
✅ 可见
Q11:当前版本不可见怎么办?
答:
通过版本链
↓
寻找历史版本
↓
继续使用Read View判断
↓
直到找到可见版本
Q12:RC 和 RR 的 Read View 有什么区别?
答:
READ COMMITTED
↓
每次快照读通常创建新的Read View
REPEATABLE READ
↓
通常复用事务首次快照读建立的Read View
Q13:Read View 用于当前读吗?
主要用于:
快照读
当前读:
读取当前最新版本
+
加锁
⚠️ 面试易错点
1. max_trx_id 不是 m_ids 最大值
错误:
m_ids = [20,30,40]
max_trx_id = 40
不要这么记。
应该理解:
max_trx_id
=
创建Read View时
下一个将要分配的事务ID
2. 事务ID大不代表一定没提交
例如:
事务20执行很慢
事务21执行很快
可能:
事务20还没提交
事务21已经提交
所以:
需要:
m_ids
判断事务在创建 Read View 时是否活跃。
3. Read View 不是直接保存所有数据
不要理解成:
Read View把数据库所有数据复制一份
不是。
Read View 主要保存:
事务可见性判断需要的信息
然后结合:
trx_id
版本链
undo log
找到合适的数据版本。
❓ 自测
-
Read View 是什么?
-
Read View 为什么存在?
-
Read View 有哪4个核心字段?
-
m_ids 是什么?
-
min_trx_id 是什么?
-
max_trx_id 是什么?
-
max_trx_id 是不是 m_ids 的最大值?
-
creator_trx_id 是什么?
-
为什么自己修改的数据自己可以看到?
-
trx_id < min_trx_id 为什么可见?
-
trx_id >= max_trx_id 为什么不可见?
-
trx_id 在 m_ids 中说明什么?
-
trx_id 不在 m_ids 中说明什么?
-
当前版本不可见怎么办?
-
版本链和 Read View 分别负责什么?
-
为什么数据库最新值是500,当前事务还能看到1000?
-
READ COMMITTED 为什么可能不可重复读?
-
REPEATABLE READ 为什么能够实现可重复读?
-
RC 和 RR 在 Read View 上有什么重要区别?
-
Read View 主要用于快照读还是当前读?
-
普通 SELECT 和 SELECT ... FOR UPDATE 有什么区别?
-
Read View 会不会把整个数据库复制一份?