Read View 在 MVCC 里如何工作的(ReadView是什么,什么时候创建)

ReadView 的定义与作用

ReadView(读视图)是 MVCC(多版本并发控制) 的核心机制,用于 确定事务在快照读时能够看到数据库中的哪些数据版本。它本质上是一个事务在某一时间点对数据库状态的快照,记录了事务启动时系统的活跃事务信息,并通过规则判断数据版本的可见性,从而解决读写冲突并实现事务隔离性

核心组成参数

ReadView 包含以下关键信息:
creator_trx_id:创建该 ReadView 的事务 ID。

  • m_ids:生成 ReadView 时,系统中所有活跃(未提交)事务的 ID 列表。
  • min_trx_id:活跃事务中的最小事务 ID。
  • max_trx_id:系统即将分配的下一个事务 ID(即当前最大事务 ID +1)。

可见性规则

通过上述参数,ReadView 对数据版本的可见性进行判断:
规则 1:若数据版本的事务 ID < min_trx_id,说明该版本在 ReadView 创建前已提交,可见
规则 2:若事务 ID ≥ max_trx_id,说明该版本在 ReadView 创建后生成,不可见
规则 3:若事务 ID 在 m_ids 中,说明该版本由未提交事务生成,不可见
规则 4:若事务 ID 在 min_trx_id 和 max_trx_id 之间且不在 m_ids 中,说明该版本已提交 ,可见

ReadView 的创建时机

ReadView 的创建与 事务隔离级别 密切相关,不同级别下规则不同:

  1. 读已提交(Read Committed, RC)
    每次快照读时创建新的 ReadView。
    效果:事务内每次查询都能看到其他事务已提交的最新数据,可能引发 不可重复读(多次读取同一数据结果不一致)。
    示例:事务 A 第一次查询数据时生成 ReadView,事务 B 提交后,事务 A 再次查询会生成新的 ReadView,从而读取到 B 提交的数据。
  2. 可重复读(Repeatable Read, RR)
    事务第一次快照读时创建 ReadView,后续复用该视图。
    效果:事务内多次读取同一数据时结果一致,解决了不可重复读问题,但可能因版本链回溯导致 幻读(需结合间隙锁解决) 1 5 7。
    示例:事务 A 第一次查询生成 ReadView,即使事务 B 在之后插入新数据并提交,事务 A 再次查询仍无法看到新数据。
  3. 串行化(Serializable)
    不使用 ReadView,直接通过加锁实现完全隔离,所有操作串行执行,避免所有并发问题(如脏读、幻读)但性能最低。

ReadView 的创建流程

以 可重复读(RR) 为例:

  1. 事务启动:事务 ID 被分配,但尚未生成 ReadView。
  2. 首次快照读:执行第一个 SELECT 语句时生成 ReadView,记录当前活跃事务列表和事务 ID 范围。
  3. 后续操作:所有快照读均复用该 ReadView,通过版本链回溯找到符合可见性规则的旧版本数据

ReadView 的实际应用示例

假设数据库中存在一条数据,其版本链如下:

版本链:事务 30(已提交) → 事务 40(未提交) → 事务 50(已提交)

  • 事务 A(ID=60)在 RR 隔离级别下读取数据:
    生成 ReadView,记录 m_ids=[60](假设无其他活跃事务)。
    沿版本链回溯:
    事务 50 的 ID 在 ReadView 中 max_trx_id 范围内,但不在 m_ids 中,说明已提交,可见。
    最终读取到事务 50 提交的版本。

总结
ReadView 是什么:事务在快照读时生成的数据库状态快照,用于判断数据版本的可见性。
何时创建:
RC 级别:每次快照读时创建。
RR 级别:事务首次快照读时创建,后续复用。
核心价值:通过版本链和可见性规则实现无锁的读写并发控制,兼顾性能与隔离性

分析一下那个例子

  1. 数据版本链的正确结构
    假设某行数据经过多次修改,其版本链按 事务提交顺序 从新到旧排列如下(箭头方向为回滚指针 DB_ROLL_PTR 指向旧版本):
最新版本 ← 事务50(已提交)
            ↑
          事务40(未提交)
            ↑
最旧版本 ← 事务30(已提交)

事务30:最初提交的数据版本。
事务40:在事务30基础上修改,但尚未提交(活跃事务)。
事务50:在事务40基础上修改,且已提交。
2. 事务A(ID=60)的 ReadView 生成
事务A的隔离级别:可重复读(RR)。
ReadView 创建时机:事务A第一次执行快照读时生成。
ReadView 参数:

creator_trx_id = 60(事务A自身ID)。
m_ids = [40, 60](假设此时活跃事务包括事务40和事务A自己)。
min_trx_id = 40(活跃事务中的最小ID)。
max_trx_id = 61(系统即将分配的下一个事务ID)。
  1. 事务A读取数据的可见性判断
    事务A执行 SELECT 语句时,沿版本链从新到旧(事务50 → 事务40 → 事务30)依次判断每个版本的可见性:
    Step 1:检查事务50的版本
    事务50的 DB_TRX_ID = 50。
    应用可见性规则:
50 < min_trx_id(40)? → 否。
50 >= max_trx_id(61)? → 否。
50 是否在 m_ids([40,60])中? → 否。
结论:可见。

Step 2:直接返回事务50的版本
由于事务50的版本已可见,无需继续回溯旧版本。

  1. 关键疑问解答
    为什么事务50的版本可见?
    根据规则,事务50的 ID(50)满足:
    介于 min_trx_id(40)和 max_trx_id(61)之间。
    不在活跃事务列表 m_ids([40,60])中 → 说明事务50已提交。
    事务40的版本为何被跳过?
    即使事务40的版本在链中,但事务A的 ReadView 判断逻辑是 从最新版本开始检查,一旦找到可见版本即停止回溯。事务50的版本已可见,无需检查事务40和事务30。
    事务40未提交会影响结果吗?
    不影响。因为事务50的版本是基于事务40的修改提交的(已覆盖事务40的未提交状态),且事务50已提交,对事务A可见。

再例如:

场景设定
事务 A(ID=51):修改数据(未提交 → 提交)。
事务 B(ID=52):多次读取数据。
初始数据:由事务 ID=50 提交,值为 100 万。
隔离级别:可重复读(RR)。
  1. 事务启动与 ReadView 生成
    (1) 事务 A 和 B 的启动顺序
时间轴:
T1 → 事务 A(ID=51)启动  
T2 → 事务 B(ID=52)启动  
T3 → 事务 A 修改数据(未提交)  
T4 → 事务 B 第一次读取  
T5 → 事务 B 第二次读取  
T6 → 事务 A 提交  
T7 → 事务 B 第三次读取

(2) ReadView 参数
image
2. 数据版本链的演化
(1) 初始状态(事务 ID=50)

版本链:
最新版本 ← 事务50(已提交,值=100万)

(2) 事务 A 修改数据(未提交)
事务 A(ID=51)执行 UPDATE,生成新版本并链接到旧版本:

版本链:
最新版本 ← 事务51(未提交,值=200万)  
            ↑  
旧版本 ← 事务50(已提交,值=100万)

(3) 事务 A 提交后
事务 A 提交,版本链不变,但事务51状态变为已提交:

版本链:
最新版本 ← 事务51(已提交,值=200万)  
            ↑  
旧版本 ← 事务50(已提交,值=100万)
  1. 事务 B 的三次读取操作分析
    (1) 第一次读取(T4)
    • 版本链:仅事务50的版本(100万)。
    • 可见性判断:
      事务50的 ID=50 < 事务 B 的 min_trx_id=51 → 可见。
    • 结果:读取到 100 万。
      image
      (2) 第二次读取(T5,事务 A 未提交)
    • 版本链:事务51(未提交)→ 事务50(已提交)。
    • 可见性判断:
      检查事务51的版本(200万):
      事务51的 ID=51 ∈ m_ids=[51,52] → 不可见。
      回溯到事务50的版本(100万):
      事务50的 ID=50 < min_trx_id=51 → 可见。
    • 结果:仍读取到 100 万。
      image

(3) 第三次读取(T7,事务 A 已提交)
- 版本链:事务51(已提交)→ 事务50(已提交)。
- 可见性判断:
检查事务51的版本(200万):
事务51的 ID=51 ∈ m_ids=[51,52] → 不可见(ReadView 未更新)。
回溯到事务50的版本(100万):
事务50的 ID=50 < min_trx_id=51 → 可见。
- 结果:仍读取到 100 万。

image
4. 关键机制总结
(1) ReadView 的冻结性
在 RR 隔离级别下,事务的 ReadView 在第一次快照读时生成,后续操作复用该视图。
事务 B 的 ReadView 参数不会更新,即使事务 A 已提交,仍认为事务 A 是活跃的(ID=51 ∈ m_ids)。
(2) 版本链遍历规则
从最新版本向旧版本回溯,找到第一个可见的版本后停止。
可见性规则优先级:

  • 事务 ID < min_trx_id → 可见。
  • 事务 ID ∈ m_ids → 不可见(活跃事务)。
  • 其他情况 → 可见(已提交事务)。

(3) 可重复读的本质
通过 冻结 ReadView 和 版本链回溯,确保事务内多次读取的数据一致性。
避免不可重复读:事务 B 无法看到事务 A 提交的修改(200万)。
image

image
根据这个小技巧你可以很快的得知此版本是否可见。

如果当前的事务ID在绿色部分,是已经提交事务,则数据可见
如果当前的事务ID在蓝色部分,会有俩种情况,如果当前事务ID在read-view数组内,是没有提交的事务不可见,如果不在数组内数据可见
如果落在红色部分,则不考虑,对于未来的事情不去想即可。

他的结构是这样的:
image

posted @ 2025-02-25 16:18  lipu123  阅读(107)  评论(0)    收藏  举报