MVCC 多版本并发控制

mysql 📚 learning mvcc · mvcc · snapshot-read · readview · isolation · innodb · undo-log

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_ID6 字节最后修改这行的事务 ID
DB_ROLL_PTR7 字节回滚指针,指向 undo log 里的上一个版本
DB_DELETE_MARK1 字节删除标记

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 种情况)

#条件结果原因
1trx_id == creator✅ 可见自己改的
2trx_id < min_trx_id✅ 可见早于所有活动事务,已提交
3trx_id ≥ max_trx_id❌ 不可见ReadView 之后才出现
4min ≤ trx_id < max,在 m_ids❌ 不可见在活动列表,可能未提交
5min ≤ 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 --> A
undo 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)

暂无

⚡ 对比(Contrast)

事务核心机制 (ACID)— MVCC 是事务 ACID 里的 'I' 隔离性实现方案
🎯 概念 📏 规则 ⚠️ 误区 🔍 追问 ✨ 口诀 共 0 张卡,点击翻面