5.5.2 MVCC 多版本实现

MySQL MVCC 多版本实现完全指南

MVCC 的"多版本"并非抽象概念,而是实实在在地存储在磁盘和内存中的物理数据结构 。它由聚簇索引行记录Undo Log 段回滚指针共同构成一个完整的版本链。本文将深入 InnoDB 底层,剖析多版本是如何被创建、链接、读取和回收的。


一、多版本的物理载体:聚簇索引 + 隐藏字段

多版本的"当前版本"存储在聚簇索引(Clustered Index)的叶子节点 中,而"历史版本"则存储在 Undo Log 段中。两者通过指针串联。

1.1 行记录的隐藏字段(物理存在)

InnoDB 中每行聚簇索引记录(Row Format)都包含三个用户不可见的隐藏列:

隐藏字段 长度 作用
DB_TRX_ID 6 字节 最近修改该行的事务 ID。每次更新都会更新为此值。
DB_ROLL_PTR 7 字节 回滚指针,指向该行在 Undo Log 中的前一个版本的物理地址。
DB_ROW_ID 6 字节 行 ID,仅在表无主键且无唯一非空索引时,InnoDB 用它作为聚簇索引键。

关键点 :事务 ID(DB_TRX_ID)是全局递增 的。修改操作(INSERT/UPDATE/DELETE)只有在真正修改数据时才会分配事务 ID,只读事务不分配。

1.2 版本链的物理结构

复制代码
当前行(聚簇索引叶子节点)
┌─────────────────────────────┐
│  id = 1, name = '张三'      │
│  age = 22                   │
│  DB_TRX_ID = 10             │  ← 最后修改的事务ID
│  DB_ROLL_PTR = 0x00A1 ──────┼──┐
└─────────────────────────────┘  │
                                 │
                                 ▼
                    ┌─────────────────────────┐
                    │   Undo Log 段 (磁盘)     │
                    │   version 2 (trx_id=5)  │
                    │   name='张三', age=21    │
                    │   DB_ROLL_PTR = 0x00B2 ──┼──┐
                    └─────────────────────────┘  │
                                                 │
                                                 ▼
                                    ┌─────────────────────────┐
                                    │   Undo Log 段 (磁盘)     │
                                    │   version 1 (trx_id=3)  │
                                    │   name='张三', age=20    │
                                    │   DB_ROLL_PTR = NULL    │
                                    └─────────────────────────┘

这就是单向链表结构,最新版本在最前面(当前行),历史版本按修改时间倒序排列。

二、多版本的生成机制:Undo Log 的两种类型

InnoDB 为不同的操作生成不同类型的 Undo Log,这决定了它们的生命周期。

Undo 类型 触发操作 存储内容 生命周期
TRX_UNDO_INSERT INSERT 操作 新行的主键(用于回滚时删除) 事务提交后立即被 Purge 线程物理删除(因为 Insert 记录对其他事务不可见,无需用于 MVCC 快照读)
TRX_UNDO_UPDATE UPDATEDELETE 操作 修改前的完整行数据副本(旧版本) 事务提交后不能立即删除,需等待所有可能读取该历史版本的 Read View 关闭后,由 Purge 线程清理

DELETE 的真相DELETE 操作不会立即物理删除数据。InnoDB 会将当前行标记为"已删除"(标记 DELETE_MASK),并将旧行数据写入 Undo Log。实际的物理删除由 Purge 线程异步完成。

三、DML 操作如何生成新版本(逐步拆解)

3.1 INSERT 操作

sql 复制代码
INSERT INTO user (id, name, age) VALUES (1, '张三', 20);
  • 在聚簇索引中插入新行。
  • 设置 DB_TRX_ID = 当前事务 ID(假设为 3)。
  • 设置 DB_ROLL_PTR = NULL(无历史版本)。
  • 生成 TRX_UNDO_INSERT 类型 Undo Log(仅记录主键 1)。
  • 事务提交后,Insert Undo Log 立即释放,无法被用于快照读。

3.2 UPDATE 操作(关键步骤)

sql 复制代码
UPDATE user SET age = 21 WHERE id = 1;   -- 当前事务 ID = 5

执行步骤

  1. 加排他锁(X Lock) 锁定该行。
  2. 将当前行完整数据(age=20, DB_TRX_ID=3) 拷贝到 Undo Log 段,生成 TRX_UNDO_UPDATE 日志。
  3. 修改当前行 :将 age 改为 21,DB_TRX_ID 改为 5,DB_ROLL_PTR 指向第 2 步生成的 Undo Log 地址。
  4. 写入 Redo Log(保证持久性)。
  5. 释放排他锁(实际在事务提交时释放)。

3.3 DELETE 操作

sql 复制代码
DELETE FROM user WHERE id = 1;   -- 当前事务 ID = 8

执行步骤

  1. 加排他锁锁定该行。
  2. 将当前行完整数据拷贝到 Undo Log,生成 TRX_UNDO_UPDATE 类型日志(删除视为特殊的更新,因为需要保留旧版本供 MVCC 使用)。
  3. 标记当前行的 DELETE_BIT(删除标记位)为 1,但物理上仍保留该行在聚簇索引中。
  4. 修改 DB_TRX_ID = 8,DB_ROLL_PTR 指向 Undo Log。
  5. 事务提交后,该行对 Read View 不可见(因已标记删除),但物理删除由 Purge 线程延迟执行。

四、多版本的读取:版本链遍历算法

当执行快照读(普通 SELECT)时,InnoDB 遵循严格的可见性判断算法 ,从当前行开始,沿着 DB_ROLL_PTR 逐个版本回溯,直到找到第一个对当前 Read View 可见的版本。

判断逻辑(简化版伪代码)

复制代码
对于版本链中的每个版本,其 trx_id = V:

如果 V == 当前事务ID(自己改的)→ 可见(返回该版本)
如果 V < min_trx_id(Read View中最小活跃事务ID)→ 可见(已提交)
如果 V >= max_trx_id(下一个分配的事务ID)→ 不可见(未来事务,继续回溯)
如果 V 在 m_ids(活跃事务列表)中 → 不可见(未提交,继续回溯)
否则 → 可见(已提交)

如果当前版本不可见,则沿 DB_ROLL_PTR 回溯到上一个 Undo Log 版本,重复上述过程。

RC 与 RR 的遍历区别

  • RC :每次 SELECT 都新建 Read View,所以每次都会从版本链头部开始,寻找最新已提交的版本。这导致了"不可重复读"------两次查询可能找到不同版本。
  • RR :事务第一次 SELECT 时创建 Read View,后续复用。因此即使其他事务提交了新版本,Read View 中的 m_ids 依旧包含它们(因为事务启动时它们活跃),所以遍历时会跳过这些新版本,直接找到旧的可见版本,保证结果一致。

五、多版本的清理:Purge 线程

历史版本不能无限增长,否则磁盘会被撑爆。Purge 线程负责物理删除不再需要的 Undo Log 版本。

5.1 判断逻辑(哪个版本可以删?)

当一个 Undo Log 记录满足以下所有条件时,可被物理清除:

  1. 该事务已经提交或回滚。
  2. 没有任何当前活跃的 Read View 需要访问该 Undo Log 版本。
    • 系统会维护一个最小活跃事务 ID(low_limit_id 。如果一个 Undo Log 的 trx_id 小于当前所有活跃事务的最小 trx_id,则所有 Read View 都不可能再读取到它,可以安全删除。

5.2 删除过程

  1. Purge 线程从 Undo Log 段中找到可清理的历史版本。
  2. 如果是 DELETE 标记的行,执行物理删除(从聚簇索引叶子节点中移除)。
  3. 如果是 UPDATE 的旧版本,直接从 Undo Log 段中回收空间(标记为可重用)。
  4. 同时,如果该版本是辅助索引(Secondary Index)的记录,也要删除对应的辅助索引条目。

性能陷阱 :如果存在长事务(长时间不提交),系统的最小活跃事务 ID 会非常靠前,导致 Purge 线程无法清理这些事务之后产生的所有 Undo Log,Undo 表空间会持续膨胀,直到磁盘写满。

六、辅助索引(Secondary Index)如何处理多版本?

重要差异 :辅助索引不包含 DB_TRX_IDDB_ROLL_PTR 隐藏字段。这意味着 MVCC 无法直接通过辅助索引判断版本可见性。

处理策略

  1. 当前读 (如 SELECT ... FOR UPDATE):通过辅助索引找到主键,再回表到聚簇索引加锁。
  2. 快照读 (普通 SELECT):
    • 从辅助索引 B+ 树中定位到叶子节点(包含主键值)。
    • 根据主键回表到聚簇索引。
    • 在聚簇索引的版本链上执行前述可见性判断,找到合适的可见版本。

性能影响:如果辅助索引查询范围很大,回表次数多,且版本链很长,会导致严重的随机 I/O。

七、多版本实现总结

维度 说明
存储位置 当前版本 → 聚簇索引叶子节点;历史版本 → Undo Log 段(系统表空间/独立 Undo 表空间)
版本链结构 聚簇索引的 DB_ROLL_PTR 指向 Undo Log 版本,Undo Log 版本间也通过指针串联,形成从新到旧的单向链表
版本生成 INSERT 生成轻量级 Insert Undo(提交即删);UPDATE/DELETE 生成 Update Undo(需保留供 MVCC 使用)
版本读取 通过 Read View + DB_TRX_ID 对比,从链头向后遍历,寻找第一个可见版本
版本回收 Purge 线程异步执行,基于系统最小活跃事务 ID 决定清理哪些旧版本
核心风险 长事务会阻塞 Purge,导致 Undo 膨胀和性能退化

八、核心流程图(逻辑示意)

#mermaid-svg-rrMOm38I4Gsdm5NU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rrMOm38I4Gsdm5NU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rrMOm38I4Gsdm5NU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rrMOm38I4Gsdm5NU .error-icon{fill:#552222;}#mermaid-svg-rrMOm38I4Gsdm5NU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rrMOm38I4Gsdm5NU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rrMOm38I4Gsdm5NU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rrMOm38I4Gsdm5NU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rrMOm38I4Gsdm5NU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rrMOm38I4Gsdm5NU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rrMOm38I4Gsdm5NU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rrMOm38I4Gsdm5NU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rrMOm38I4Gsdm5NU .marker.cross{stroke:#333333;}#mermaid-svg-rrMOm38I4Gsdm5NU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rrMOm38I4Gsdm5NU p{margin:0;}#mermaid-svg-rrMOm38I4Gsdm5NU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rrMOm38I4Gsdm5NU .cluster-label text{fill:#333;}#mermaid-svg-rrMOm38I4Gsdm5NU .cluster-label span{color:#333;}#mermaid-svg-rrMOm38I4Gsdm5NU .cluster-label span p{background-color:transparent;}#mermaid-svg-rrMOm38I4Gsdm5NU .label text,#mermaid-svg-rrMOm38I4Gsdm5NU span{fill:#333;color:#333;}#mermaid-svg-rrMOm38I4Gsdm5NU .node rect,#mermaid-svg-rrMOm38I4Gsdm5NU .node circle,#mermaid-svg-rrMOm38I4Gsdm5NU .node ellipse,#mermaid-svg-rrMOm38I4Gsdm5NU .node polygon,#mermaid-svg-rrMOm38I4Gsdm5NU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rrMOm38I4Gsdm5NU .rough-node .label text,#mermaid-svg-rrMOm38I4Gsdm5NU .node .label text,#mermaid-svg-rrMOm38I4Gsdm5NU .image-shape .label,#mermaid-svg-rrMOm38I4Gsdm5NU .icon-shape .label{text-anchor:middle;}#mermaid-svg-rrMOm38I4Gsdm5NU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rrMOm38I4Gsdm5NU .rough-node .label,#mermaid-svg-rrMOm38I4Gsdm5NU .node .label,#mermaid-svg-rrMOm38I4Gsdm5NU .image-shape .label,#mermaid-svg-rrMOm38I4Gsdm5NU .icon-shape .label{text-align:center;}#mermaid-svg-rrMOm38I4Gsdm5NU .node.clickable{cursor:pointer;}#mermaid-svg-rrMOm38I4Gsdm5NU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rrMOm38I4Gsdm5NU .arrowheadPath{fill:#333333;}#mermaid-svg-rrMOm38I4Gsdm5NU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rrMOm38I4Gsdm5NU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rrMOm38I4Gsdm5NU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rrMOm38I4Gsdm5NU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rrMOm38I4Gsdm5NU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rrMOm38I4Gsdm5NU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rrMOm38I4Gsdm5NU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rrMOm38I4Gsdm5NU .cluster text{fill:#333;}#mermaid-svg-rrMOm38I4Gsdm5NU .cluster span{color:#333;}#mermaid-svg-rrMOm38I4Gsdm5NU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rrMOm38I4Gsdm5NU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rrMOm38I4Gsdm5NU rect.text{fill:none;stroke-width:0;}#mermaid-svg-rrMOm38I4Gsdm5NU .icon-shape,#mermaid-svg-rrMOm38I4Gsdm5NU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rrMOm38I4Gsdm5NU .icon-shape p,#mermaid-svg-rrMOm38I4Gsdm5NU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rrMOm38I4Gsdm5NU .icon-shape .label rect,#mermaid-svg-rrMOm38I4Gsdm5NU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rrMOm38I4Gsdm5NU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rrMOm38I4Gsdm5NU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rrMOm38I4Gsdm5NU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是



用户发起快照读 SELECT
生成 Read View
读取聚簇索引当前行
当前行是否对 Read View 可见?
直接返回当前行数据
通过 DB_ROLL_PTR 定位到 Undo Log 版本
Undo Log 版本是否可见?
返回该历史版本数据
继续回溯上一个 Undo Log 版本

一句话总结 MVCC 多版本实现

MVCC 的多版本本质上是"用空间换时间"------在聚簇索引中保留最新行,在 Undo Log 段中通过回滚指针串联旧版本链,借助事务 ID 和 Read View 实现无锁快照读,再通过后台 Purge 线程异步回收过期的历史版本。理解这条链的生成、遍历和回收,是掌握 MySQL 事务与高性能并发优化的关键。

相关推荐
学长毕业设计6 小时前
基于SpringBoot的公益基金管理系统(源码+文档+讲解视频)
java·spring boot·后端
东小西6 小时前
【SAA实战】第 3 篇 · 工具调用全攻略:把业务能力交给 Agent 自己调度
java·后端·spring
东小西6 小时前
【SAA实战】第 4 篇 · Agent 短期记忆:saver 让 Agent 跨轮记得住(threadId 隔离)
java·后端·spring
oradh6 小时前
Oracle TX 锁 Mode 4(Share)问题排查总结
数据库·oracle·tx 锁 mode 4·oracle tx 锁
许彰午6 小时前
22-DataCenter报文序列化
java·低代码·架构·状态模式
2601_962065256 小时前
[MySQL] SQL优化之性能分析
java·sql·mysql
yi.Ist7 小时前
数据定义语言-DDL操作
数据库·学习·mysql·oracle·大海豚
阿kun要赚马内7 小时前
MySQL 索引基础
后端·mysql
小范同学_7 小时前
JDK1.7 与 JDK1.8 HashMap 底层原理对比 + 数组并发扩容死循环详解
java·开发语言
予昊8 小时前
从零实现“在线五子棋对战“:WebSocket 实时通信 + 段位匹配
java·开发语言·网络·websocket