1. 本文范围与参考
本文基于 MySQL 8.0(以 8.0.35 为讨论基线)InnoDB 的 MVCC(多版本并发控制) 与 一致性读 语义,聚焦:
- ReadView 各字段含义及与 全局事务 id 的关系;
- 聚簇索引行 与 undo 版本链;
- 本事务修改 与 他事务版本 在可见性判断上的不同路径;
- RR(REPEATABLE READ) 与 RC(READ COMMITTED) 下 ReadView 生命周期 的差异;
- 事务 id 尚未分配(常为 0) 时,不会以 0 进入活跃事务 id 列表;分配后才以正式 id 参与活跃事务管理。
官方文档入口:
以下对源码字段的命名以教学常用归纳为主,与 storage/innodb 中结构体字段名可能略有出入,语义与手册一致。
2. 为什么需要 MVCC
InnoDB 在不加锁的 SELECT(一致性非锁定读)下,需要回答:
当前会话应该看到该行哪一个已提交/未提交版本?
若直接读最新聚簇记录,会与并发事务的未提交写冲突;若全部加锁,并发度下降。MVCC 通过保留历史版本(undo)与可见性判断(ReadView),使读操作在多数场景下不阻塞写,写不阻塞读(在可接受的隔离级别语义下)。
3. 一行数据与 undo 版本链
3.1 聚簇索引记录上的关键信息(概念)
对聚簇索引上一行(简化):
| 概念 | 含义 |
|---|---|
| trx_id | 最近一次修改该行的事务 id(已分配后的全局事务号)。 |
| roll_pointer | 指向 undo 中本条修改对应的回滚段信息,并可沿链找到更旧版本。 |
3.2 版本链(逻辑)
flowchart LR
subgraph 当前记录
R0["聚簇行: trx_id = T2\nroll_pointer →"]
end
subgraph undo
U1["undo 记录 1\n旧 trx_id / 旧指针"]
U2["undo 记录 2\n更旧..."]
end
R0 --> U1 --> U2
快照读若判定「当前记录版本」不可见,则沿 roll_pointer 在 undo 中构造/定位更旧版本,重复可见性判断,直到可见或无更早版本。
4. ReadView 是什么
ReadView 是某次一致性读在某一时刻对「系统中事务 id 状态」的快照,用于判断:别的会话提交的版本,相对本次读是否应被看见。
4.1 常见字段语义(教学归纳)
| 字段(常见叫法) | 含义 |
|---|---|
| creator_trx_id | 创建本 ReadView 的事务在创建时刻引擎中的 trx_id;若尚未分配,常为 0。 |
| m_ids | 创建 ReadView 瞬间,系统中已分配 trx_id 且仍处于活跃的事务 id 集合(不含「未分配」概念上的 0)。 |
| up_limit_id | 通常与 m_ids 中最小 trx_id 相关(实现中用于快速比较)。 |
| low_limit_id | 通常表示:创建 ReadView 时,「下一个将被分配」的 trx_id(有的资料写作 max_trx_id + 1 语义)。≥ low_limit_id 的事务 id 视为在本 ReadView 快照之后才出现。 |
4.2 事务 id 未分配时:0 不会进入 m_ids
| 阶段 | trx->id | 是否进入 m_ids |
|---|---|---|
| 尚未向全局申请事务号 | 0(或未占用正式 id) | 不会把 0 当作一个正常 trx_id 写入 m_ids。 |
| 已分配,例如 T | T | T 进入活跃事务管理;提交/回滚后退出。 |
因此:活跃 id 列表里只会出现真实分配过的 id,不会出现「0 代表当前会话」 这种项。
5. 可见性判断的两条路径(核心)
对聚簇记录上某版本 trx_id = X:
路径 A:是否为本事务修改(优先)
flowchart TD
A[读取行版本 trx_id = X] --> B{X == 当前事务对象 trx->id ?}
B -->|是| V[可见:本事务修改的版本\n与 ReadView 中 m_ids 规则无冲突]
B -->|否| C[进入路径 B:用 ReadView + undo]
- 相等时:认定为当前事务产生的版本,一致性读需能读到(含未提交,对自己可见)。
- 此处比较的是 执行该次读时 的
trx->id与 行上 X,不依赖「ReadView 创建时 creator 是否为 0」的数值相等。
路径 B:他人或历史版本(ReadView + undo)
对 X ≠ 当前 trx->id 的版本,用 ReadView 判断相对快照是否可见;不可见则沿 undo 找旧版。
简化逻辑(与手册/源码教学版一致):
- 若 X < up_limit_id 且不在 m_ids 的语义范围内(实现细节略):多表示较早已结束事务的版本,往往可见(需结合删除标记等,此处不展开)。
- 若 X ≥ low_limit_id:表示该 id 在 ReadView 创建之后才出现 → 不可见 → 找 undo。
- 若 up_limit_id ≤ X < low_limit_id 且 X ∈ m_ids 且 X 不是本事务:他人未提交 → 不可见 → 找 undo。
- 若 X 不在 m_ids 且小于 low_limit_id:通常表示已提交 → 可见。
教学时常强调:本事务走 路径 A;别人走 路径 B。
6. RR 与 RC 下 ReadView 的生命周期
| 隔离级别 | 一致性读与 ReadView |
|---|---|
| RR | 同一事务内,第一次一致性读创建 ReadView,之后复用(同一事务内多次快照读同一快照)。 |
| RC | 每条一致性读语句可能创建新 ReadView(语句级快照)。 |
sequenceDiagram
participant S as 会话
participant RV as ReadView
Note over S,RV: RR
S->>RV: 第 1 次一致性读 → 创建 RV1
S->>RV: 第 2 次一致性读 → 仍用 RV1
Note over S,RV: RC
S->>RV: 第 1 条 SELECT → 创建 RVa
S->>RV: 第 2 条 SELECT → 创建 RVb(新快照)
7. 数值案例:先快照读、再写、再快照读(RR)
前提:全局已有活跃事务 48、49;创建 ReadView 前一刻「下一个将分配的 id」为 50。
| 步骤 | 操作 | trx->id | ReadView |
|---|---|---|---|
| 1 | BEGIN | 未分配 / 0 | 无 |
| 2 | 第 1 次 SELECT(一致性读) | 可能仍为 0 或在此路径分配 id(实现相关) | 创建 RV1;creator 可能为 0;m_ids 含 48、49,不含 0 |
| 3 | UPDATE 某行 | 分配 50 | 仍只有 RV1(RR 不重建) |
| 4 | 第 2 次 SELECT | 50 | 仍用 RV1 |
对 UPDATE 后该行:行上 trx_id = 50。
- 路径 A:50 == 当前 trx->id (50) → 可见。
- 若误用「行.trx_id 必须等于 RV1.creator_trx_id」且 creator 仍为 0,会逻辑错误;正确做法是 路径 A 用当前
trx->id。
8. 与「库存超卖」类问题的关系(延伸)
MVCC + RR 只解决读的可见性,不自动保证业务不变量(如库存 ≥ 0)。并发扣库存需 UPDATE ... WHERE stock >= 1 条件更新、行锁或乐观锁等,见业务层设计,本文不展开。
9. 小结
- undo 提供版本链;ReadView 提供对别人版本的快照边界。
- 本事务修改的版本:行.trx_id 与 当前 trx->id 一致则可见,不走「仅 ReadView 活跃表」那一套。
- 0 不是合法的全局 trx_id 入 m_ids;分配后的 T 才进入活跃事务管理。
- RR 复用第一次 ReadView;RC 语句级新快照。
10. 参考与延伸阅读
- MySQL 8.0 Reference Manual — InnoDB Transaction Model
- 源码阅读建议关键词:
ReadView、row_vers、changes_visible、MVCC
本文为 DBA 内部学习整理,生产问题请结合 SHOW ENGINE INNODB STATUS、错误日志与官方手册排查。