深度拆解:从 Read View 到 Undo Log,多版本并发控制(MVCC)的底层确定性

摘要

在关系型数据库(如 MySQL InnoDB)的高并发场景下,"读写冲突"是调优面临的最常见瓶颈。如果为了保证数据一致性而对读写操作全部加锁(如强行使用串行化读),系统的吞吐量将发生灾难性下跌。为了实现"读不加锁,读写不冲突",现代主流存储引擎普遍采用了 MVCC(Multi-Version Concurrency Control,多版本并发控制) 机制。本文将从多版本链表、Undo Log 演进以及 Read View 结构出发,深度剖析 MVCC 在事务隔离中的底层实现。

一、 数据行的隐蔽面孔:聚集索引中的隐藏列

在 InnoDB 存储引擎中,你在表里看到的每一行记录(Row),在底层的 B+ 树聚集索引中除了存放你自定义的列数据之外,还会被编译器强制附加三个极其关键的系统隐藏列

隐藏列名称 占用空间 核心职责
DB_TRX_ID 6 字节 事务 ID。记录最后一次插入或修改该行数据的事务系统标识。
DB_ROLL_PTR 7 字节 回滚指针 。指向该行数据上一个版本的 Undo Log 记录,是构建历史版本的时空纽带。
DB_ROW_ID 6 字节 行单调自增 ID。仅在表没有显式指定主键或唯一索引时由内核自动生成。

这三个隐藏列是实现事务回滚与多版本控制的物理底座。

二、 时光回溯的通道:Undo Log 版本链

每当一个事务尝试修改一条记录时,为了支持并发事务读取历史数据(以及本事务回滚),存储引擎不会直接将旧数据覆盖并抹去,而是会遵循以下串联逻辑:

  1. 加锁改写:事务 A 对该行记录加锁,准备修改。

  2. 写 Undo 日志 :把该行记录当前的旧版本值原封不动地复制到一块专门的内存/磁盘区域------Undo Log(回滚日志) 中。

  3. 更新记录与指针 :修改聚集索引页中该行记录的实际值,将 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 列表中               │                           │
 └───────────────────────────┴─────────────────────────────┴───────────────────────────┘
  1. trx_id<min_trx_id : 说明生成这个版本的事务在当前只读事务开启前已经完全提交了。结论:该版本数据可见

  2. trx_id≥max_trx_id : 说明生成这个版本的事务是在当前只读事务开启之后才启动的(属于未来的事务)。结论:该版本数据绝对不可见

  3. trx_id=creator_trx_id : 说明这个版本就是当前只读事务自己修改的。结论:自己看自己的修改,必然可见

  4. min_trx_id≤trx_id<max_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. 读已提交(RC,Read Committed)

在 RC 隔离级别下,事务中每一次执行 SELECT 语句时,都会重新、独立地生成一个全新的 Read View 。 这意味着,如果另一个并发写事务在两次 SELECT 之间提交了数据,第二次 SELECT 生成的 Read View 里的 m_ids 就会剔除掉这个写事务 ID。根据算法,写事务的修改变得可见,这就实现了"读已提交",但同时也导致了"不可重复读"的发生。

2. 可重复读(RR,Repeatable Read)

在 RR 隔离级别下,事务只有在第一次执行 SELECT 时才会生成一个 Read View ,并且在整个事务的生命周期内一直复用 这个视图。 即使后续有其他写事务提交了,由于当前事务手中的 Read View 已经固化,活跃事务列表 m_ids 没有任何变化。因此,无论执行多少次查询,顺着版本链推导出的结果都完全一致,在底层完美阻断了不可重复读的发生。

五、 总结

  1. MVCC 是现代主流数据库存储引擎(如 InnoDB)消除读写锁竞争、最大化并发吞吐吞吐量的核心引擎。

  2. 通过向聚集索引行记录强制追加隐藏列,配合向后追加写的 Undo Log,在物理上编织出了一条严密的数据时空追溯链条。

  3. Read View 结构通过最小活跃事务 ID、未来事务上限等数学边界,实现了高效的可见性过滤,并在内核级别通过调节 Read View 的刷新时机,轻量级地撑起了 RC 与 RR 隔离级别的物理隔离防线。

相关推荐
froyoisle2 小时前
CSP 真题解析:[CSP-J 2025-T3] 异或和
c++·算法·csp·算法竞赛·信奥赛
迈巴赫车主2 小时前
Prim堆优化
数据结构·算法·prim
郝学胜-神的一滴2 小时前
干货版《算法导论》08:哈希——重构集合数据结构的速度魔法
数据结构·python·程序人生·算法·重构·软件构建·哈希算法
计算机安禾2 小时前
【算法分析与设计】第50篇:量子计算模型下的算法概览
算法·量子计算
人道领域2 小时前
【LeetCode刷题日记】39.组合总和&&40.组合总和Ⅱ
算法·leetcode·回溯算法
通信小呆呆2 小时前
从理想到现实:实际系统中非理想特性及其补偿方法
算法·数学建模·信号处理
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第97题】【Mysql篇】第27题:说说分库与分表的设计?
java·开发语言·数据库·分布式·mysql·算法
yuan199972 小时前
双目视觉测距实现
算法
洒脱的六边形战士加辣2 小时前
Java排序方法全解析
java·数据结构·算法