5.5.2 MVCC 多版本实现 MySQL MVCC 多版本实现完全指南MVCC 的“多版本”并非抽象概念而是实实在在地存储在磁盘和内存中的物理数据结构。它由聚簇索引行记录、Undo Log 段和回滚指针共同构成一个完整的版本链。本文将深入 InnoDB 底层剖析多版本是如何被创建、链接、读取和回收的。一、多版本的物理载体聚簇索引 隐藏字段多版本的“当前版本”存储在聚簇索引Clustered Index的叶子节点中而“历史版本”则存储在Undo Log 段中。两者通过指针串联。1.1 行记录的隐藏字段物理存在InnoDB 中每行聚簇索引记录Row Format都包含三个用户不可见的隐藏列隐藏字段长度作用DB_TRX_ID6 字节最近修改该行的事务 ID。每次更新都会更新为此值。DB_ROLL_PTR7 字节回滚指针指向该行在 Undo Log 中的前一个版本的物理地址。DB_ROW_ID6 字节行 ID仅在表无主键且无唯一非空索引时InnoDB 用它作为聚簇索引键。关键点事务 IDDB_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_id5) │ │ name张三, age21 │ │ DB_ROLL_PTR 0x00B2 ──┼──┐ └─────────────────────────┘ │ │ ▼ ┌─────────────────────────┐ │ Undo Log 段 (磁盘) │ │ version 1 (trx_id3) │ │ name张三, age20 │ │ DB_ROLL_PTR NULL │ └─────────────────────────┘这就是单向链表结构最新版本在最前面当前行历史版本按修改时间倒序排列。二、多版本的生成机制Undo Log 的两种类型InnoDB 为不同的操作生成不同类型的 Undo Log这决定了它们的生命周期。Undo 类型触发操作存储内容生命周期TRX_UNDO_INSERTINSERT操作新行的主键用于回滚时删除事务提交后立即被 Purge 线程物理删除因为 Insert 记录对其他事务不可见无需用于 MVCC 快照读TRX_UNDO_UPDATEUPDATE或DELETE操作修改前的完整行数据副本旧版本事务提交后不能立即删除需等待所有可能读取该历史版本的 Read View 关闭后由 Purge 线程清理DELETE 的真相DELETE操作不会立即物理删除数据。InnoDB 会将当前行标记为“已删除”标记DELETE_MASK并将旧行数据写入 Undo Log。实际的物理删除由 Purge 线程异步完成。三、DML 操作如何生成新版本逐步拆解3.1 INSERT 操作INSERTINTOuser(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 操作关键步骤UPDATEuserSETage21WHEREid1;-- 当前事务 ID 5执行步骤加排他锁X Lock锁定该行。将当前行完整数据age20, DB_TRX_ID3拷贝到 Undo Log 段生成TRX_UNDO_UPDATE日志。修改当前行将age改为 21DB_TRX_ID改为 5DB_ROLL_PTR指向第 2 步生成的 Undo Log 地址。写入 Redo Log保证持久性。释放排他锁实际在事务提交时释放。3.3 DELETE 操作DELETEFROMuserWHEREid1;-- 当前事务 ID 8执行步骤加排他锁锁定该行。将当前行完整数据拷贝到 Undo Log生成TRX_UNDO_UPDATE类型日志删除视为特殊的更新因为需要保留旧版本供 MVCC 使用。标记当前行的DELETE_BIT删除标记位为 1但物理上仍保留该行在聚簇索引中。修改DB_TRX_ID 8DB_ROLL_PTR指向 Undo Log。事务提交后该行对Read View不可见因已标记删除但物理删除由 Purge 线程延迟执行。四、多版本的读取版本链遍历算法当执行快照读普通SELECT时InnoDB 遵循严格的可见性判断算法从当前行开始沿着DB_ROLL_PTR逐个版本回溯直到找到第一个对当前 Read View 可见的版本。判断逻辑简化版伪代码对于版本链中的每个版本其 trx_id V 如果 V 当前事务ID自己改的→ 可见返回该版本 如果 V min_trx_idRead 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 记录满足以下所有条件时可被物理清除该事务已经提交或回滚。没有任何当前活跃的 Read View需要访问该 Undo Log 版本。系统会维护一个最小活跃事务 IDlow_limit_id。如果一个 Undo Log 的trx_id小于当前所有活跃事务的最小trx_id则所有 Read View 都不可能再读取到它可以安全删除。5.2 删除过程Purge 线程从 Undo Log 段中找到可清理的历史版本。如果是DELETE标记的行执行物理删除从聚簇索引叶子节点中移除。如果是UPDATE的旧版本直接从 Undo Log 段中回收空间标记为可重用。同时如果该版本是辅助索引Secondary Index的记录也要删除对应的辅助索引条目。性能陷阱如果存在长事务长时间不提交系统的最小活跃事务 ID 会非常靠前导致 Purge 线程无法清理这些事务之后产生的所有 Undo LogUndo 表空间会持续膨胀直到磁盘写满。六、辅助索引Secondary Index如何处理多版本重要差异辅助索引不包含DB_TRX_ID和DB_ROLL_PTR隐藏字段。这意味着 MVCC 无法直接通过辅助索引判断版本可见性。处理策略当前读如SELECT ... FOR UPDATE通过辅助索引找到主键再回表到聚簇索引加锁。快照读普通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 膨胀和性能退化八、核心流程图逻辑示意是否是否用户发起快照读 SELECT生成 Read View读取聚簇索引当前行当前行是否对 Read View 可见?直接返回当前行数据通过 DB_ROLL_PTR 定位到 Undo Log 版本Undo Log 版本是否可见?返回该历史版本数据继续回溯上一个 Undo Log 版本一句话总结 MVCC 多版本实现MVCC 的多版本本质上是“用空间换时间”——在聚簇索引中保留最新行在 Undo Log 段中通过回滚指针串联旧版本链借助事务 ID 和 Read View 实现无锁快照读再通过后台 Purge 线程异步回收过期的历史版本。理解这条链的生成、遍历和回收是掌握 MySQL 事务与高性能并发优化的关键。