MVCC 中的可见性判断规则

总结

本文深入解析 MVCC 中的可见性判断规则,并结合示例进行说明。

简介

在支持 MVCC(多版本并发控制) 的数据库系统中(如 MySQL 的 InnoDB 引擎),每行数据可能有多个版本,每个版本都记录了修改它的事务 ID(DB_TRX_ID)。为了实现事务隔离性,数据库会为每个一致性读操作创建一个 Read View(即:一致性读视图)。

可见性判断规则是 MVCC 的核心逻辑之一,事务根据当前的 Read View,判断某个数据版本是否对自己可见。这不仅决定了事务能否读取到该数据,还影响 事务隔离级别 行为(如可重复读和读已提交)。

MVCC 可见性判断的规则

前置知识

MySQL InnoDB 引擎中每一行数据都隐藏了两个字段:

  • DB_TRX_ID:创建或最后一次修改该行数据的事务 ID。
  • DB_ROLL_PTR:回滚指针,指向该行的上一个版本在 Undo Log 中的位置。

Read View 主要包含以下关键字段:

  • m_ids:当前活跃事务的 ID 列表(即尚未提交的事务)。
  • min_trx_id:当前活跃事务中最小的事务 ID。
  • max_trx_id:下一个将要分配的事务 ID(即当前已分配的最大事务 ID + 1)。
  • creator_trx_id:创建视图的事务 ID。

Read View 创建时机:

  • READ COMMITTED(RC):每次快照读生成新的一致性视图。
  • REPEATABLE READ(RR):整个事务期间使用同一个一致性视图。

可见性判断规则(RC、RR 隔离级别)

当事务 A(如:TRX_ID = 101)读取某一行数据时,它会获取该行当前版本的 DB_TRX_ID,并根据当前的 Read View 中的信息进行可见性判断。

1. 当前事务自己修改的数据,始终可见

  • 条件:DB_TRX_ID 等于当前事务 ID
  • 结论:✅ 可见
  • 说明:即使事务尚未提交,也能看到自己修改的数据。
  • 示例:事务 A 修改某行数据但未提交 → 再次读取时能看到自己的修改(无论 RC 还是 RR)

2. 在 Read View 创建之前已提交的事务修改的数据,可见

  • 条件:DB_TRX_ID 小于 min_trx_id
  • 结论:✅ 可见
  • 说明:这些事务在 Read View 创建时已经提交。
  • 示例:
    • 在 RC 中,每次读取都生成新的 Read View,因此这个“已提交事务”是相对于当前这次读操作的 Read View 而言的。
    • 在 RR 中,这个“已提交事务”是相对于事务第一次读操作的 Read View 而言的。

3. Read View 创建之后才开始的事务修改的数据,当前 Read View 不可见

  • 条件:DB_TRX_ID 大于等于 max_trx_id
  • 结论:当前 Read View 不可见
  • 说明:这些事务是在 Read View 创建之后才开始的,不属于当前一致性视图。
  • 示例:
    • 事务 A(RC)第一次读时事务 105 未提交 → 不可见。事务 105 提交后,事务 A 第二次读 → 生成新的 Read View,可以看到事务 105 的修改。
    • 事务 A(RR)第一次读时事务 105 未提交 → 不可见。事务 105 提交后,事务 A 第二次读 → 仍不可见(Read View 未变)。

4. 介于 min_trx_id 和 max_trx_id 之间的事务修改的数据

  • 条件:min_trx_id ≤ DB_TRX_ID < max_trx_id
  • 判断方式:检查 DB_TRX_ID 是否在活跃事务列表 m_ids 中
    • 在列表中 → 事务尚未提交,数据不可见
    • 不在列表中 → 事务已提交,数据可见
  • 示例:
    • 事务 A(RC)第一次读时,事务 102 正在运行(活跃)→ 不可见。
    • 事务 102 提交后,事务 A 第二次读 → 活跃事务列表中不含 102 → 可见。

5. 如果当前版本不可见,则追溯更早的版本

  • 操作:沿着版本链(通过 DB_ROLL_PTR)向前查找,重复以上判断
  • 目的:找到一个对当前事务可见的数据版本
  • 结束条件:找到可见版本,或到达版本链的末尾(到达末尾未能找到就返回空)

示例:版本链回溯与可见性判断详细过程

以 RR 隔离级别示例,假设当前事务 A 的事务 ID 是 TRX_ID = 101,它执行了一个一致性读操作,此时 Read View 中的信息如下:

  • m_ids = [100, 102]
  • min_trx_id = 100
  • max_trx_id = 103
  • creator_trx_id=101

假设此时 Undo Log 有以下几个版本的数据:

数据版本 DB_TRX_ID 是否可见 判断依据 DB_ROLL_PTR(回滚指针:指向前一版本)
版本 5 103 ❌ 不可见 大于等于 max_trx_id → 版本 4
版本 4 102 ❌ 不可见 在活跃事务列表中 → 版本 3
版本 3 101 ✅ 可见 是当前事务自己修改的 → 版本 2
版本 2 100 ❌ 不可见 在活跃事务列表中 → 版本 1
版本 1 99 ✅ 可见 小于 min_trx_id,已提交 → NULL(链表末尾)

事务 A 会从当前版本(最新版本:版本 5)开始,沿着版本链逐级回溯,直到找到一个对自己可见的版本或到达链表末尾。。

第一步:判断版本 5(DB_TRX_ID = 103)

  • 103 ≥ max_trx_id(103) → 条件成立
  • 结论:❌ 不可见
  • 操作:通过 DB_ROLL_PTR 指针,回溯到前一个版本(版本 4)

第二步:判断版本 4(DB_TRX_ID = 102)

  • 100 ≤ 102 < 103 → 在 min 和 max 之间
  • 查看是否在活跃事务列表 m_ids = [100, 102] 中 → 存在
  • 结论:❌ 不可见
  • 操作:继续回溯到前一个版本(版本 3)

第三步:判断版本 3(DB_TRX_ID = 101)

  • DB_TRX_ID = 当前事务 ID(101)
  • 结论:✅ 可见
  • 操作:找到可见版本,读取该行数据,回溯结束

关联文章


文章作者: huan
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 huan !
  目录