MVCC机制

InnoDB MVCC 机制详解

一、MVCC 是什么

MVCC(Multi-Version Concurrency Control,多版本并发控制) 是 InnoDB 为实现高并发读写而采用的核心机制。它的基本思想是:

写操作不覆盖旧数据,而是生成新版本;读操作依据自己的"快照"规则,选择对自身可见的版本。

在 InnoDB 中,MVCC 主要服务于 READ COMMITTED(RC)和 REPEATABLE READ(RR)两个隔离级别下的普通 SELECT 查询(即"快照读"),让读操作在不加锁的情况下,也能读到一致性视图,从而实现:

  • 读不阻塞写
  • 写不阻塞读

二、MVCC 解决什么问题

2.1 没有 MVCC 时的困境

传统并发控制依赖加锁:读加共享锁、写加排他锁,读写互相阻塞。在读多写少的 OLTP 场景中,大量读请求会被写操作阻塞,吞吐量受限。

2.2 MVCC 的解法

写操作创建新版本而非原地覆盖,读操作读取事务开始时刻的一致性快照。读写作用于不同版本,自然不再互相等待。

2.3 MVCC 的能力边界

MVCC 解决的是读写冲突 ,以下问题它不解决:

问题 说明
写写冲突 两个事务同时改同一行最新版本,仍需行锁串行化
当前读的幻读 SELECT ... FOR UPDATE、UPDATE、DELETE 等当前读需配合 Next-Key Lock
写偏斜(Write Skew) 快照隔离 ≠ 可串行化,MVCC 本身不检测写偏斜
长事务负担 长事务的旧 Read View 会"钉住"旧版本,导致 Undo Log 无法清理

三、核心组件

InnoDB 的 MVCC 由三个组件协同实现:隐藏列 、Undo Log 版本链 、Read View。

3.1 隐藏列

InnoDB 在每行记录上默默维护着与 MVCC 直接相关的隐藏字段:

隐藏列 大小 作用
DB_TRX_ID 6 字节 记录最后一次插入或修改该行数据的事务 ID
DB_ROLL_PTR 7 字节 回滚指针,指向 Undo Log 中该行上一个版本的位置
DB_ROW_ID 6 字节 行 ID(仅在表无主键且无唯一索引时生成,与 MVCC 无关)

注意 :DELETE 在 InnoDB 中不是物理删除,而是通过标记位将行"标记为已删除",真正的清除由后台 Purge 线程完成。

3.2 DB_ROLL_PTR 的存储结构

DB_ROLL_PTR 是一个 7 字节(56 位)的编码地址,内部包含定位一条 Undo 记录所需的全部坐标:

位段 长度 含义
低位偏移量(Offset) 16 位 Undo 记录在所在 Undo 页面内的偏移位置
页号(Page Number) 32 位 Undo 记录所在的 Undo 页面编号
回滚段 ID(Rollback Segment ID) 7 位 Undo 记录所属的回滚段
最高位标志(Insert Flag) 1 位 标识 Undo 记录类型是 INSERT 还是 UPDATE

3.3 Undo Log 与版本链

写入时机

Undo Log 的数据不是事务提交时才写入的 ,而是在执行 INSERT、UPDATE、DELETE 的过程中实时写入的。具体流程:

  1. 先将"如何撤销这次修改"所需的信息写入 Undo Log;
  2. 再修改当前行的用户字段为新值;
  3. 更新当前行的 DB_TRX_ID 为当前事务 ID;
  4. 让 DB_ROLL_PTR 指向刚写入的 Undo 记录。
两类 Undo 的清理差异
Undo 类型 用途 清理时机
INSERT Undo 仅事务回滚时需要 事务提交后即可丢弃(不需要为其他事务提供历史版本)
UPDATE/DELETE Undo 回滚 + MVCC 一致性读 只要还有活跃事务的 Read View 可能需要它,就不能被清理;确认无快照需要后由 Purge 线程删除
版本链的形成

一行数据被多个事务连续修改时,每次修改都在 Undo Log 中留下一条记录,DB_ROLL_PTR 像链条一样把各历史版本串联起来:

复制代码
当前行(最新版本)
  │  DB_TRX_ID = 200(未提交)
  │  DB_ROLL_PTR ──┐
  ▼                │
Undo 记录 v2       │
  DB_TRX_ID = 100  │
  DB_ROLL_PTR ──┐  │
  ▼             │  │
Undo 记录 v1    │  │
  DB_TRX_ID = 80│  │
  DB_ROLL_PTR = NULL

链头是当前最新数据,链尾是最早的旧数据。版本链上每个节点都同时带有 DB_TRX_ID(裁判)和 DB_ROLL_PTR(向导),各自在不同环节发挥作用。

3.4 Read View

版本链提供了"有哪些版本",Read View 则决定"当前事务能看见哪个版本"。它是事务进行快照读时生成的一个可见性规则集,包含四个核心字段:

字段 含义
m_ids 生成 Read View 时,所有活跃(已启动未提交)读写事务的 ID 集合
min_trx_id m_ids 中的最小事务 ID
max_trx_id 系统下一个要分配的事务 ID(高水位线)
creator_trx_id 创建该 Read View 的事务 ID

四、可见性判断规则

有了 Read View,InnoDB 在读取一行数据时,从版本链的最新记录开始 ,用该记录的 DB_TRX_ID 与 Read View 的规则逐一比对,寻找第一个可见的版本:

序号 条件 结论
1 DB_TRX_ID == creator_trx_id 是当前事务自己的修改,可见
2 DB_TRX_ID < min_trx_id 修改该版本的事务在 Read View 生成前已提交,可见
3 DB_TRX_ID >= max_trx_id 修改该版本的事务在 Read View 生成后才启动,不可见
4 DB_TRX_ID 在 m_ids 中 修改该版本的事务在 Read View 生成时仍活跃(未提交),不可见
5 DB_TRX_ID 不在 m_ids 中,且介于 min_trx_id 和 max_trx_id 之间 该事务在 Read View 生成前已提交,可见

判断流程:

  1. 定位当前行最新版本,读取其 DB_TRX_ID;
  2. 与 Read View 比对:可见则直接返回当前行值,不碰 Undo Log;
  3. 不可见则顺着 DB_ROLL_PTR 回溯到上一个 Undo 版本 ,读该版本的 DB_TRX_ID 继续比对;
  4. 重复直到找到可见版本或版本链结束。

关键理解 :所谓"读当前行还是读 Undo",不是二选一的选择,而是版本链遍历的结果。最新版本可见就读当前行,不可见就回溯 Undo 直到找到可见版本。


五、RC 与 RR 的实现差异

MVCC 在 RC 和 RR 下的可见性判断算法完全相同 ,唯一的区别在于 Read View 的生成时机:

隔离级别 Read View 生成时机 效果
READ COMMITTED (RC) 每次 执行 SELECT 时都生成一个新的 Read View 能读到其他事务在本次 SELECT 前已提交的最新数据,因此会出现"不可重复读"
REPEATABLE READ (RR) 第一次 执行 SELECT 时生成,之后整个事务复用这个 Read View 整个事务期间看到同一个快照,即使其他事务提交也看不到,保证"可重复读"

RR 级别下 MVCC 解决的是快照读 的幻读问题;对于当前读 (SELECT ... FOR UPDATE、UPDATE、DELETE),InnoDB 还需配合 Next-Key Lock 来处理。


六、一次查询的完整过程(示例)

假设行 R 初始值为 v1,事务 100 将其改为 v2 并提交,事务 200 又将其改为 v3 但未提交。

当前行状态 :用户字段 = v3,DB_TRX_ID = 200,DB_ROLL_PTR → Undo 记录(含 v2,DB_TRX_ID = 100,再指向含 v1 的 Undo)。

场景 A:事务 300 查询(Read View 中 200 活跃、100 已提交)

  1. 当前行 DB_TRX_ID = 200 → 在活跃列表 m_ids 中 → 不可见;
  2. 顺 DB_ROLL_PTR 回溯到 v2 版本,DB_TRX_ID = 100 → 已提交 → 可见;
  3. 返回 v2。

场景 B:事务 400 查询(在事务 200 提交后生成 Read View)

  1. 当前行 DB_TRX_ID = 200 → 已提交 → 可见;
  2. 直接返回 v3,不回溯 Undo Log。

七、辅助机制与注意事项

7.1 Purge 机制

DELETE 或 UPDATE 产生的旧版本不会立即消失。后台 Purge 线程 会在确认没有任何活跃事务的 Read View 还需要这些旧版本之后,才物理清理它们。

  • 如果存在长事务 ,其旧 Read View 会"钉住"旧版本,导致 Undo Log 无法清理,引发 History list 膨胀和磁盘空间增长。
  • 因此"避免长事务"在 MVCC 数据库中不是风格建议,而是架构约束。

7.2 辅助索引的差异

与聚簇索引不同,辅助索引记录不包含 DB_TRX_ID 和 DB_ROLL_PTR 两个隐藏列。当通过辅助索引进行一致性读时,如果发现索引页上的记录可能已被更新,InnoDB 需要回表到聚集索引,利用聚集索引记录的版本链信息判断可见性并构建正确的数据版本。

7.3 MVCC 仅在 RC / RR 下生效

隔离级别 是否使用 MVCC
READ UNCOMMITTED 否,直接读最新版本
READ COMMITTED 是,每次 SELECT 生成新 Read View
REPEATABLE READ 是,首次 SELECT 生成并复用 Read View
SERIALIZABLE 否,靠锁实现

八、核心概念速查表

概念 一句话解释
DB_TRX_ID "裁判":记录最后修改该行的事务 ID,用于判断当前版本是否可见
DB_ROLL_PTR "向导":7 字节编码地址,指向 Undo Log 中上一个版本的位置
Undo Log 存储旧版本数据,UPDATE/DELETE 的 Undo 需服务 MVCC,INSERT 的 Undo 提交后即可清理
版本链 由 DB_ROLL_PTR 串联的从最新到最旧的历史版本序列
Read View 可见性规则集,包含 m_ids、min_trx_id、max_trx_id、creator_trx_id
快照读 普通 SELECT,走 MVCC,不加锁
当前读 SELECT ... FOR UPDATE、UPDATE、DELETE,读最新版本并加锁
Purge 后台线程,确认无快照需要后清理旧 Undo 版本

九、总结

InnoDB 的 MVCC 通过 隐藏列 + Undo Log 版本链 + Read View 三者协同,实现了"读不阻塞写、写不阻塞读":

  1. 写操作 :将旧值写入 Undo Log,更新当前行的 DB_TRX_ID,DB_ROLL_PTR 指向新 Undo 记录,形成版本链;
  2. 读操作 :从最新版本开始,用 DB_TRX_ID 与 Read View 比对判断可见性,不可见则顺 DB_ROLL_PTR 回溯,直到找到可见版本;
  3. RC 与 RR 的差异:仅在于 Read View 的生成时机(每条 SELECT vs 首次 SELECT);
  4. 代价:长事务会阻塞 Purge 导致 Undo 膨胀;快照隔离存在写偏斜的理论缺口;写写冲突仍需锁解决。
相关推荐
xixingzhe231 分钟前
mysql全量、增量备份
数据库·mysql
AllData公司负责人8 小时前
AI大模型数据开发底座系列说明书,AllData集成Chat2DB开源项目建设AI 数据库分析底座,涵盖AI 洞察/对话SQL/智能视图!
数据库·人工智能·sql·开源·ai 洞察·对话sql
xixingzhe29 小时前
数据库备份的分类与特点
数据库
时间的拾荒人12 小时前
Redis 主从复制详解:配置实操 + 原理机制
数据库·redis·面试
隔窗听雨眠13 小时前
MySQL三表JOIN执行内核与优化器决策机制
数据库·mysql·sql优化
Decay__13 小时前
SQLServer 索引与 SQL 优化 —— 聚集索引、非聚集索引、覆盖索引、碎片维护与执行计划
运维·数据库·sql·mysql·架构
笃行35015 小时前
【金仓数据库产品体验官】订单增量同步实战:MERGE、OUTPUT 与 IDENTITY 的兼容验证
数据库
星恒随风18 小时前
MySQL 从零入门(六):复合查询详解——多表查询、自连接、子查询与 UNION
linux·服务器·数据库·mysql
无奈笑天下19 小时前
银河麒麟上lsblk命令教程详解
linux·运维·服务器·数据库