TL;DR(30 秒扫完)
- MVCC = 每行数据保留多个历史版本,读者看到自己需要的版本,互不干扰
- 三件套:行隐藏字段(
DB_TRX_ID/DB_ROLL_PTR/DB_DELETE_MARK)+ undo log 版本链 + ReadView - 可见性判定:trx_id < min → 可见;≥ max → 不可见;中间看 m_ids
- RC vs RR 唯一区别:ReadView 生成时机(每次 SELECT vs 事务首次 SELECT)
- MVCC 只用于快照读,当前读走锁
关键结论
结论 AMVCC 让快照读不加锁,是 InnoDB 高并发的关键
结论 B当前读(
SELECT FOR UPDATE / UPDATE / DELETE)不走 MVCC,看最新已提交版本结论 C长事务阻碍 undo log 清理(purge),导致空间膨胀
结论 DRR 的幻读只能"部分缓解",不能消除(快照+当前混用时会遇到)
完整讲解(费曼四步)
STEP 1 · 概念
MVCC(Multi-Version Concurrency Control)是多版本并发控制。核心思想:读不阻塞写、写不阻塞读——通过给每行数据保留多个历史版本,让读事务看到自己需要的版本,而不是被锁阻塞。STEP 2 · 大白话
维基百科比喻: 每次有人修改维基百科词条,都不覆盖原版,而是新增一个版本。你打开词条时,看到的版本取决于你打开的那一刻"哪些修改已经保存"。MVCC 让数据库每行都变成一个"维基百科"——多个版本共存,读者只看到自己该看的那一版。STEP 3 · 底层
行隐藏字段(三个字段)
| 字段 | 大小 | 含义 |
|---|---|---|
DB_TRX_ID | 6 字节 | 最后修改这行的事务 ID |
DB_ROLL_PTR | 7 字节 | 回滚指针,指向 undo log 里的上一个版本 |
DB_DELETE_MARK | 1 字节 | 删除标记 |
undo log 版本链
当前行版本 → undo 版本 → undo 版本 → ... → 初始版本
(最新版本) (最早的)
每次 UPDATE 生成新版本,旧版本挂到 undo log 链上。
ReadView 四个字段
ReadView = {
m_ids: [创建 ReadView 时仍在活动的事务 ID 列表],
min_trx_id: m_ids 中最小的,
max_trx_id: 下一个要分配的事务 ID (max+1),
creator_trx_id: 创建者自己
}
可见性判定规则(5 种情况)
| # | 条件 | 结果 | 原因 |
|---|---|---|---|
| 1 | trx_id == creator | ✅ 可见 | 自己改的 |
| 2 | trx_id < min_trx_id | ✅ 可见 | 早于所有活动事务,已提交 |
| 3 | trx_id ≥ max_trx_id | ❌ 不可见 | ReadView 之后才出现 |
| 4 | min ≤ trx_id < max,在 m_ids | ❌ 不可见 | 在活动列表,可能未提交 |
| 5 | min ≤ trx_id < max,不在 m_ids | ✅ 可见 | 不在活动列表说明已提交 |
DB_ROLL_PTR 找上一版继续判断。
RC vs RR 的差别(唯一区别)
| 隔离级别 | ReadView 生成时机 | 效果 |
|---|---|---|
| RC | 每次 SELECT 新建 | 每次看到最新已提交 |
| RR | 事务首次 SELECT 生成一次 | 整个事务看到一致快照 |
STEP 4 · 简化
一句话总结:MVCC = 行隐藏字段 + undo 链 + ReadView。用事务 ID 判断哪一版可见。RC 每次新 ReadView,RR 事务首次生成一次。 记忆口诀:- trx_id 小于 min → 读
- trx_id 大于等于 max → 不读
- 中间:在 m_ids 不读,不在就读
常见误区
说"trx_id 小的不读"
trx_id 小应该读(先提交的可见)
说"MVCC 用于所有读"
MVCC 只用于快照读,当前读走锁
说"MVCC 只用于 RR"
RC 也用 MVCC,只是 ReadView 生成时机不同
说"RR 完全解决幻读"
RR 大部分解决,快照+当前混用仍有幻读
延伸追问
长事务为什么导致 undo log 膨胀?
长事务持有旧 ReadView,purge 线程不敢清理被引用的旧版本,undo 链越长空间占用越多。
SELECT FOR UPDATE 走 MVCC 吗?不走。当前读直接看最新已提交,加 X 锁。MVCC 只服务快照读。
RC 每次新建 ReadView 为什么就能看到最新值?
新建的 m_ids 是当前活动的,之前已提交的都不在 m_ids 里,符合"已提交"可见条件。
RR 下的幻读怎么触发?
快照读 + 当前读混用:先 SELECT 后 SELECT FOR UPDATE,中间其他事务 INSERT → 当前读看到新行。
速查表
三件套: 隐藏字段 + undo 链 + ReadView
ReadView: m_ids / min / max / creator
可见性: < min 可见 / ≥ max 不可见 / 中间看 m_ids
RC vs RR: 每次新建 ReadView vs 首次生成一次
快照读: 普通 SELECT
当前读: SELECT FOR UPDATE / UPDATE / DELETE(不走 MVCC)
Anki 候选卡片
Q: MVCC 的三个行隐藏字段?
A: DB_TRX_ID / DB_ROLL_PTR / DB_DELETE_MARK
Q: ReadView 的四个字段?
A: m_ids / min_trx_id / max_trx_id / creator_trx_id
Q: 可见性判定:trx_id < min 时?
A: 可见(早于所有活动事务)
Q: RC 和 RR 的唯一区别?
A: ReadView 生成时机——RC 每次新建,RR 首次生成一次
Q: MVCC 用于哪种读?
A: 快照读;当前读(SELECT FOR UPDATE / UPDATE / DELETE)不走
关联题目
- ⚠️ 《如何理解 MVCC?》— Round 2 B3, ⭐⭐(可见性规则答反了)
- 《二级索引在索引覆盖时如何使用 MVCC?》— 待考
关联知识
让读不阻塞写、写不阻塞读——行版本 + ReadView + 快照读三件套
维基百科:每次修改不覆盖原版,读者看到自己打开那一刻的版本
✦ 记 忆 口 诀 ✦
trx_id < min → 读;≥ max → 不读;中间看 m_ids
关键可视化
可见性判定流程图
flowchart TD
A[拿到行版本 trx_id] --> B{trx_id == creator?}
B -->|是| C[✅ 可见<br/>自己改的]
B -->|否| D{trx_id < min_trx_id?}
D -->|是| E[✅ 可见<br/>早于所有活动事务]
D -->|否| F{trx_id >= max_trx_id?}
F -->|是| G[❌ 不可见<br/>ReadView 后出现]
F -->|否| H{在 m_ids?}
H -->|是| I[❌ 不可见<br/>在活动列表]
H -->|否| J[✅ 可见<br/>不在活动列表已提交]
G --> K[沿 DB_ROLL_PTR<br/>找上一版]
I --> K
K --> Aundo log 版本链
flowchart LR A[当前行版本] --> B[undo v1] --> C[undo v2] --> D[undo v3] --> E[初始版本] style A fill:#161b22,stroke:#58a6ff,stroke-width:2px,color:#58a6ff style B fill:#161b22,stroke:#8b949e,color:#8b949e style C fill:#161b22,stroke:#8b949e,color:#8b949e style D fill:#161b22,stroke:#8b949e,color:#8b949e style E fill:#161b22,stroke:#8b949e,color:#8b949e
费曼式可视化(完整图解)
↗ 新窗口打开知识关系
⬆️ 前置(Prerequisite)
mysql-transaction-core🔄 延伸(Extends)
暂无
🎯 概念
📏 规则
⚠️ 误区
🔍 追问
✨ 口诀
共 0 张卡,点击翻面