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 事务与高性能并发优化的关键。

相关推荐
EntyIU3 小时前
NFS离线部署
java
祈禾4 小时前
Redis三大缓存问题与分布式锁
运维·数据库·redis·笔记·分布式·缓存
DBA小马哥5 小时前
关系型数据库核心概念手册:SQL、事务与存储引擎的技术脉络
数据库·sql
丫头,冲鸭!!!5 小时前
记账网站3-连数据库
数据库·个人开发
灯澜忆梦5 小时前
【MySQL12】进阶篇 | SQL优化
数据库·sql·mysql·性能优化
IvorySQL5 小时前
PostgreSQL 日报|PG18.5 回归测试崩溃问题(8 月 12 日)
大数据·数据库·人工智能·postgresql
ltl5 小时前
学习型查询优化器:Neo、Bao、Balsa 与 LLM-CBO
数据库
ltl6 小时前
持久内存退场之后:ZNS SSD 与下一代非易失内存
数据库
OH_TPC6 小时前
HarmonyOS APP开发---“滤镜大师“图像处理App,需要用到这个库
java·图像处理·华为·harmonyos·鸿蒙
imaol17 小时前
链表 -- 环链表
java·前端·链表