简单易懂的方式理解MVCC--2
对,这一步就是 MVCC 最核心、也是最容易绕的地方。
你先不要背那些 m_ids / min_trx_id / max_trx_id,我们用一个具体例子把它彻底搞懂。
1. 先记住:Read View 就是一张“事务时间快照”
假设现在有 4 个事务:
事务100:已经提交
事务101:已经提交
事务102:正在执行
事务103:正在执行
现在事务 104 开始执行:
T104
↓
创建 Read View
它会记录类似:
当前活跃事务:
[102, 103, 104]
最小活跃事务ID:
102
下一个尚未分配的事务ID:
105
你可以暂时理解成:
事务ID时间线
100 101 102 103 104 105
| | | | | |
已提交 已提交 活跃 活跃 我自己 未来
↑ ↑
Read View 事务T104
于是 T104 就可以定义一个规则:
哪些事务修改的数据,我能看?哪些不能看?
2. InnoDB 判断一个版本,核心就是看 DB_TRX_ID
每条 InnoDB 记录背后都有一个隐藏的:
DB_TRX_ID
你可以简单理解成:
“这个版本最后是哪个事务产生的?”
比如:
id = 1
price = 3000
DB_TRX_ID = 103
说明:
这个
3000版本是事务 103 修改出来的。
然后 Undo Log 里面还有:
3000 ← T103产生
↓
2000 ← T102产生
↓
1000 ← T101产生
于是 T104 查询的时候,就开始判断:
这些版本我到底能不能看?
3. 判断规则其实可以浓缩成一句话
对于事务 T104 的 Read View:
如果这个版本是“我创建 Read View 之前已经提交的事务”产生的,我能看到。
如果是:
创建 Read View 时还没提交的事务产生的,我看不到。
如果是:
创建 Read View 之后才产生的事务,我也看不到。
于是可以把事务分成三类。
4. 第一种:事务 ID < min_trx_id
假设:
Read View:
min_trx_id = 102
那么:
事务100
事务101
都满足:
trx_id < 102
这些事务意味着:
在 Read View 创建之前就已经结束了。
所以它们产生的版本:
100 → 可见
101 → 可见
5. 第二种:trx_id >= max_trx_id
假设:
max_trx_id = 105
那么:
105
106
107
...
这些事务都是:
Read View 创建以后才出现的事务。
所以:
trx_id >= 105
↓
不可见
因为:
“你这个事务是在我拍完照片以后才发生的,我当然看不到。”
6. 第三种:最麻烦的情况
比如:
min_trx_id = 102
max_trx_id = 105
活跃事务:
[102, 103, 104]
现在有一个版本:
DB_TRX_ID = 103
它满足:
102 <= 103 < 105
这时候不能简单说可见或者不可见。
因为:
103 是在 Read View 创建的时候仍然活跃的事务。
所以:
103 ∈ m_ids
因此:
不可见。
7. 所以你可以把规则记成这个表
假设:
min_trx_id = 102
max_trx_id = 105
m_ids = [102,103,104]
| 版本的 DB_TRX_ID | 判断 | 是否可见 |
|---|---|---|
| 100 | < 102 |
✅ 可见 |
| 101 | < 102 |
✅ 可见 |
| 102 | 活跃事务 | ❌ 不可见 |
| 103 | 活跃事务 | ❌ 不可见 |
| 104 | 当前事务 | ❌ 不按当前版本看 |
| 105 | >= 105 |
❌ 不可见 |
| 106 | >= 105 |
❌ 不可见 |
这就是 Read View 的核心。
8. 现在把它放进真正的版本链
假设数据库现在有:
当前版本:
price = 3000
DB_TRX_ID = 103
↓
Undo
price = 2000
DB_TRX_ID = 102
↓
Undo
price = 1000
DB_TRX_ID = 101
T104 创建 Read View:
min_trx_id = 102
max_trx_id = 105
m_ids = [102,103,104]
T104 查询 price。
第一步:看到 3000
price = 3000
DB_TRX_ID = 103
判断:
103 在 m_ids 中
说明:
T103 在我创建 Read View 的时候还没提交。
所以:
3000 ❌ 不可见
怎么办?
沿着 Undo Log 往回找。
第二步:找到 2000
price = 2000
DB_TRX_ID = 102
判断:
102 在 m_ids 中
还是:
❌ 不可见
继续沿 Undo Log 找。
第三步:找到 1000
price = 1000
DB_TRX_ID = 101
判断:
101 < min_trx_id(102)
所以:
1000 ✅ 可见
于是:
T104最终看到:
price = 1000
这就是:
MVCC 通过 Read View + 版本链,找到当前事务能够看到的历史版本。
9. 你现在应该能理解“可重复读”了
假设:
T101:price = 1000
之后:
T102:1000 → 2000
然后:
T103:2000 → 3000
T104 开始:
创建 Read View
它看到:
3000 → T103
2000 → T102
1000 → T101
因为:
T102、T103
在 T104 创建 Read View 时都是活跃事务。
所以:
3000 ❌
2000 ❌
1000 ✅
最终:
T104看到1000
10. 这时候你就能理解 Read View 到底是什么了
Read View 不是一份数据副本。
它更像一个:
“版本过滤器”。
数据库里可能存在:
3000
↓
2000
↓
1000
Read View 并没有复制:
1000
而是告诉 MVCC:
“按照我的事务时间点,这几个版本哪些可以看。”
然后 MVCC:
当前版本
↓
Read View判断
↓
不可见
↓
Undo Log
↓
上一个版本
↓
Read View判断
↓
不可见
↓
Undo Log
↓
上一个版本
↓
Read View判断
↓
可见
↓
返回
11. 最重要的一个面试口诀
你可以记:
小于最小活跃事务ID → 可见
大于等于最大事务ID → 不可见
处于中间 → 看是否在活跃事务列表 m_ids 中
在 m_ids 中 → 不可见
不在 m_ids 中 → 可见
也就是:
DB_TRX_ID
│
┌─────────┼─────────┐
↓ ↓ ↓
< min 中间 >= max
│ │ │
↓ ↓ ↓
可见 查 m_ids 不可见
│
┌─────┴─────┐
↓ ↓
在里面 不在里面
↓ ↓
不可见 可见
12. 但是还有一个特别重要的坑
Read View 的具体创建时机,和隔离级别有关。
这正是为什么:
READ COMMITTED
和:
REPEATABLE READ
的 MVCC 行为不同。
最简单地记:
READ COMMITTED
↓
每次快照读都可能创建新的 Read View
↓
所以第二次可能看到别人已经提交的数据
REPEATABLE READ
↓
第一次快照读建立 Read View
↓
后续快照读复用这个 Read View
↓
所以多次读取保持一致
这一步一旦真正理解,你就把 MVCC → Read View → RR/RC → 不可重复读 全部串起来了。
如果你接下来继续学,我建议下一步专门搞懂 m_ids、min_trx_id、max_trx_id 这三个到底分别是什么,以及为什么 trx_id >= max_trx_id 一定不可见。这是 MySQL 面试里最容易被追问的地方。
浙公网安备 33010602011771号