深度拆解:从 Read View 到 Undo Log,多版本并发控制(MVCC)的底层确定性 摘要在关系型数据库如 MySQL InnoDB的高并发场景下“读写冲突”是调优面临的最常见瓶颈。如果为了保证数据一致性而对读写操作全部加锁如强行使用串行化读系统的吞吐量将发生灾难性下跌。为了实现“读不加锁读写不冲突”现代主流存储引擎普遍采用了MVCCMulti-Version Concurrency Control多版本并发控制机制。本文将从多版本链表、Undo Log 演进以及 Read View 结构出发深度剖析 MVCC 在事务隔离中的底层实现。一、 数据行的隐蔽面孔聚集索引中的隐藏列在 InnoDB 存储引擎中你在表里看到的每一行记录Row在底层的 B 树聚集索引中除了存放你自定义的列数据之外还会被编译器强制附加三个极其关键的系统隐藏列隐藏列名称占用空间核心职责DB_TRX_ID6 字节事务 ID。记录最后一次插入或修改该行数据的事务系统标识。DB_ROLL_PTR7 字节回滚指针。指向该行数据上一个版本的Undo Log记录是构建历史版本的时空纽带。DB_ROW_ID6 字节行单调自增 ID。仅在表没有显式指定主键或唯一索引时由内核自动生成。这三个隐藏列是实现事务回滚与多版本控制的物理底座。二、 时光回溯的通道Undo Log 版本链每当一个事务尝试修改一条记录时为了支持并发事务读取历史数据以及本事务回滚存储引擎不会直接将旧数据覆盖并抹去而是会遵循以下串联逻辑加锁改写事务 A 对该行记录加锁准备修改。写 Undo 日志把该行记录当前的旧版本值原封不动地复制到一块专门的内存/磁盘区域——Undo Log回滚日志中。更新记录与指针修改聚集索引页中该行记录的实际值将DB_TRX_ID改为事务 A 的 ID并让DB_ROLL_PTR物理指向刚刚在 Undo Log 中生成的那个旧版本节点。随着多个并发事务交错执行修改原本孤立的一行数据在底层就会通过DB_ROLL_PTR指针被拉平并串联成一条由新到旧的单向链表。这条链表就是 MVCC 赖以生存的“多版本时光链”。三、 快照读的数学边界Read View快照视图有了多版本链表当一个并发的“只读事务”发起读取请求时在快照读/Consistent Read 场景下它到底应该看链表中的哪一个版本这就需要通过Read View来进行边界判定。Read View 是在事务发起查询时由事务管理器动态创建的一个内存结构它主要包含以下四个核心字段m_ids在当前这一时刻整个数据库系统中还未提交的、活跃的事务 ID 列表。min_trx_idm_ids列表中最小的事务 ID。max_trx_id系统即将分配给下一个新事务的 ID 值即当前最大事务 ID 1。creator_trx_id生成当前这个 Read View 的只读事务自身的事务 ID。核心可见性判定算法数学状态机当只读事务遍历该行数据的 Undo Log 版本链时它会取出当前版本的DB_TRX_ID假设为trx_id并严格带入以下四条红线进行比对Plaintext┌───────────────────────────┬─────────────────────────────┬───────────────────────────┐ │ trx_id min_trx_id │ min_trx_id trx_id ... │ trx_id max_trx_id │ ├───────────────────────────┼─────────────────────────────┼───────────────────────────┤ │ 已提交事务绝对可见 │ 活跃或未提交判断是否在 │ 未来事务绝对不可见 │ │ │ m_ids 列表中 │ │ └───────────────────────────┴─────────────────────────────┴───────────────────────────┘trx_idmin_trx_id 说明生成这个版本的事务在当前只读事务开启前已经完全提交了。结论该版本数据可见。trx_id≥max_trx_id 说明生成这个版本的事务是在当前只读事务开启之后才启动的属于未来的事务。结论该版本数据绝对不可见。trx_idcreator_trx_id 说明这个版本就是当前只读事务自己修改的。结论自己看自己的修改必然可见。min_trx_id≤trx_idmax_trx_id 此时需要进一步检索m_ids数组如果trx_id在m_ids列表中说明生成这个版本的事务目前还处于活跃状态还没提交。结论不可见。如果trx_id不在m_ids列表中说明生成这个版本的事务虽然 ID 很大但在当前查询发起前已经完成了提交。结论可见。如果判定为“不可见”只读事务就会顺着DB_ROLL_PTR指针向下寻找上一个更老版本的 Undo Log 节点再次带入算法比对直到找到第一个可见的版本为止。四、 隔离级别的本质Read View 的创建时机MVCC 机制的奇妙之处在于通过精细调控 Read View 的创建时机可以用完全相同的底层代码完美实现 SQL 标准中的两种核心隔离级别1. 读已提交RCRead Committed在 RC 隔离级别下事务中每一次执行SELECT语句时都会重新、独立地生成一个全新的 Read View。 这意味着如果另一个并发写事务在两次SELECT之间提交了数据第二次SELECT生成的 Read View 里的m_ids就会剔除掉这个写事务 ID。根据算法写事务的修改变得可见这就实现了“读已提交”但同时也导致了“不可重复读”的发生。2. 可重复读RRRepeatable Read在 RR 隔离级别下事务只有在第一次执行SELECT时才会生成一个 Read View并且在整个事务的生命周期内一直复用这个视图。 即使后续有其他写事务提交了由于当前事务手中的 Read View 已经固化活跃事务列表m_ids没有任何变化。因此无论执行多少次查询顺着版本链推导出的结果都完全一致在底层完美阻断了不可重复读的发生。五、 总结MVCC 是现代主流数据库存储引擎如 InnoDB消除读写锁竞争、最大化并发吞吐吞吐量的核心引擎。通过向聚集索引行记录强制追加隐藏列配合向后追加写的 Undo Log在物理上编织出了一条严密的数据时空追溯链条。Read View 结构通过最小活跃事务 ID、未来事务上限等数学边界实现了高效的可见性过滤并在内核级别通过调节 Read View 的刷新时机轻量级地撑起了 RC 与 RR 隔离级别的物理隔离防线。