---
title: MVCC 多版本并发控制
type: concept
domain: mysql
tags: [mvcc, snapshot-read, readview, isolation, innodb, undo-log]
status: learning
created: 2026-09-17
last_reviewed: 2026-09-17
method: code-reading
related_questions:
  - "如何理解MVCC？"
  - "二级索引在索引覆盖时如何使用MVCC？"
related_knowledge:
  - ./mysql-transaction-core.md
visualizations:
  - ./mysql-acid-mvcc-feynman.html
anki_cards: 5
interview_rounds:
  - "2026-09-17-round-2-B3"
---

# MVCC 多版本并发控制

> 让读不阻塞写、写不阻塞读的魔法。核心是"行版本 + ReadView + 快照读"三件套。

## 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 只用于**快照读**，当前读走锁

## 关键结论

- **结论 A**：MVCC 让快照读不加锁，是 InnoDB 高并发的关键
- **结论 B**：当前读（`SELECT FOR UPDATE` / `UPDATE` / `DELETE`）不走 MVCC，看最新已提交版本
- **结论 C**：长事务阻碍 undo log 清理（purge），导致空间膨胀
- **结论 D**：RR 的幻读只能"部分缓解"，不能消除（快照+当前混用时会遇到）

## 完整讲解（费曼四步）

### 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 不读，不在就读

## 常见误区

- **误区 1**：说"trx_id 小的不读" → 正确：**trx_id 小应该读**（先提交的可见）
- **误区 2**：说"MVCC 用于所有读" → 正确：MVCC **只用于快照读**，当前读走锁
- **误区 3**：说"MVCC 只用于 RR" → 正确：RC 也用 MVCC，只是 ReadView 生成时机不同
- **误区 4**：说"RR 完全解决幻读" → 正确：RR 大部分解决，快照+当前混用仍有幻读

## 延伸追问

1. **长事务为什么导致 undo log 膨胀？**
   - 长事务持有旧 ReadView，purge 线程不敢清理被引用的旧版本，undo 链越长空间占用越多。
2. **`SELECT FOR UPDATE` 走 MVCC 吗？**
   - 不走。当前读直接看最新已提交，加 X 锁。MVCC 只服务快照读。
3. **RC 每次新建 ReadView 为什么就能看到最新值？**
   - 新建的 m_ids 是当前活动的，之前已提交的都不在 m_ids 里，符合"已提交"可见条件。
4. **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）
```

## 关联题目（题库）

- ⚠️ 《如何理解 MVCC？》— Round 2 B3, ⭐⭐（可见性规则答反了）
- 《二级索引在索引覆盖时如何使用 MVCC？》— 待考

## 关联知识

- [MySQL 事务核心机制](./mysql-transaction-core.md)
- [主题地图](./_moc.md)

## Anki 候选卡片

1. **正**：MVCC 的三个行隐藏字段？**反**：DB_TRX_ID / DB_ROLL_PTR / DB_DELETE_MARK
2. **正**：ReadView 的四个字段？**反**：m_ids / min_trx_id / max_trx_id / creator_trx_id
3. **正**：可见性判定：trx_id < min 时？**反**：可见（早于所有活动事务）
4. **正**：RC 和 RR 的唯一区别？**反**：ReadView 生成时机——RC 每次新建，RR 首次生成一次
5. **正**：MVCC 用于哪种读？**反**：快照读；当前读（SELECT FOR UPDATE / UPDATE / DELETE）不走

---

*最后更新：2026-09-17*
