本页内容受版权保护 · 已添加水印 · 禁止任何形式转载
事务与并发(ACID / 隔离级别 / 锁 / MVCC) · MVCC 多版本并发控制

RC 和 RR 隔离级别在 Read View 创建时机上有何不同?

What is the difference in Read View creation timing between the RC and RR isolation levels?

答案语言

考察要点

这道题面试官想看你是否深入理解 InnoDB 的 MVCC (多版本并发控制) 实现机制。核心考察点在于:

  1. Read View 的概念:你是否知道 Read View 是什么,它的内部结构和工作原理。
  2. 隔离级别的实现:你是否明白 Read Committed (RC) 和 Repeatable Read (RR) 这两种隔离级别并非通过简单的加锁,而是通过控制 Read View 的生成时机来实现的。
  3. 并发场景推演:你能否基于不同的 Read View 创建策略,准确推演出在 RC 和 RR 级别下,一个事务内多次执行 SELECT 会看到什么不同的结果,并解释其背后的原因(如不可重复读和幻读)。

核心概念

MVCC 是 InnoDB 为实现高并发事务处理而设计的核心机制,它允许读写操作同时进行而互不阻塞(在“快照读”场景下)。其实现依赖于三个关键元素:隐藏列、Undo Log 和 Read View。

  • 隐藏列:每行数据除了我们定义的列外,还有两个重要的隐藏列:
    • DB_TRX_ID:创建或最后修改该行版本的事务 ID。
    • DB_ROLL_PTR:回滚指针,指向该行上一个版本的 Undo Log 记录。
  • Undo Log:当数据被修改时,旧版本的数据会被记录到 Undo Log 中,形成一个版本链。
    mermaid
    1graph LR
    2 subgraph Undo Log Version Chain
    3 direction LR
    4 Row_v3(Row Version 3<br>trx_id=100) -->|roll_ptr| Row_v2(Row Version 2<br>trx_id=95);
    5 Row_v2 -->|roll_ptr| Row_v1(Row Version 1<br>trx_id=80);
    6 end
  • Read View (一致性视图):是事务在执行快照读时,对整个数据库在某一时刻的“快照”。它决定了事务能看到哪个版本的数据。其主要构成如下:
    • m_ids:创建 Read View 时,所有活跃(未提交)事务的 ID 列表。
    • min_trx_id:m_ids 列表中的最小事务 ID。
    • max_trx_id:创建 Read View 时,系统下一个将要分配的事务 ID。
    • creator_trx_id:创建该 Read View 的事务自身的 ID。

可见性判断规则:当一个事务访问某行数据时,会获取其最新的版本,并用该版本的 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 后,才有新事务修改了该行,不可见。需沿着 DB_ROLL_PTR 找上一个版本。
  4. 如果 min_trx_id <= DB_TRX_ID < max_trx_id,则检查 DB_TRX_ID 是否在 m_ids 列表中:
    • 若在,说明修改该行的事务在创建 Read View 时仍活跃,不可见。需找上一个版本。
    • 若不在,说明修改该行的事务在创建 Read View 时已提交,可见。

RC 和 RR 的核心区别:Read View 创建时机

1. Read Committed (RC) 读已提交

在 RC 级别下,事务中每一条 SELECT 语句执行时,都会重新创建一个新的 Read View。

示例场景: 假设 accounts 表有一行数据 (id=1, balance=1000)。

时间事务 A (Session A, RC 级别)事务 B (Session B)
T1START TRANSACTION;
T2SELECT balance FROM accounts WHERE id = 1; <br> 结果: 1000 <br> (创建 Read View 1)
T3START TRANSACTION; <br> UPDATE accounts SET balance = 1200 WHERE id = 1; <br> COMMIT;
T4SELECT balance FROM accounts WHERE id = 1; <br> 结果: 1200 <br> (创建新的 Read View 2)
T5COMMIT;

分析:

  • 在 T2,事务 A 的 SELECT 创建了 Read View 1。此时事务 B 未开始,所以能看到 balance=1000。
  • 在 T4,事务 A 再次执行 SELECT。因为是 RC 级别,它会重新创建一个 Read View 2。在创建 Read View 2 时,事务 B 已经提交,因此 balance=1200 这个版本对 Read View 2 是可见的。
  • 结果就是事务 A 在同一个事务内两次读取同一行数据,得到了不同的结果,这就是不可重复读。

2. Repeatable Read (RR) 可重复读

在 RR 级别下,只有在事务中的第一条 SELECT 语句执行时,才会创建一个 Read View,并且整个事务期间都会复用这个 Read View。

示例场景(同上):

时间事务 A (Session A, RR 级别)事务 B (Session B)
T1START TRANSACTION;
T2SELECT balance FROM accounts WHERE id = 1; <br> 结果: 1000 <br> (创建 Read View 1,并复用)
T3START TRANSACTION; <br> UPDATE accounts SET balance = 1200 WHERE id = 1; <br> COMMIT;
T4SELECT balance FROM accounts WHERE id = 1; <br> 结果: 1000 <br> (复用 T2 创建的 Read View 1)
T5COMMIT;

分析:

  • 在 T2,事务 A 的 SELECT 创建了 Read View 1。
  • 在 T4,事务 A 再次执行 SELECT。因为是 RR 级别,它会复用 T2 创建的 Read View 1。根据 Read View 1 的可见性规则,事务 B 的修改(trx_id 在 Read View 1 创建时还不存在或处于活跃状态)是不可见的。事务 A 只能看到 balance=1000 这个版本。
  • 结果就是事务 A 在同一个事务内多次读取同一行数据,得到的结果总是一致的,实现了可重复读。

常见低分回答

  1. 混淆隔离级别与锁的类型:“RC 用行锁,RR 用表锁”。这是一个非常常见的错误。RC 和 RR 在 UPDATE/DELETE 时都使用行级锁。它们在快照读(普通 SELECT)上的区别主要源于 MVCC 和 Read View 策略,而非锁的粒度。RR 只是在特定条件下(为了解决幻读)会额外使用间隙锁(Gap Lock)。
  2. 对“读最新”的模糊描述:“RC 总是能读到最新的数据”。这种说法不精确。RC 读到的是“执行 SELECT 语句时,已经提交的最新版本”,而不是数据库物理上绝对最新的版本。如果一个长事务正在修改数据但未提交,RC 也是读不到的。
  3. 认为 RR 完全解决了幻读:“RR 级别下绝对不会有幻读”。这个说法不完全正确。RR 通过 MVCC 机制避免了在“快照读”(SELECT)场景下的幻读。但在“当前读”(SELECT ... FOR UPDATE, SELECT ... LOCK IN SHARE MODE, INSERT, UPDATE, DELETE)场景下,为了避免幻读,InnoDB 需要借助 Next-Key Lock(Record Lock + Gap Lock)来实现。如果隔离级别设置不当或 SQL 写法特殊,仍可能遇到幻读问题。
  4. 无法解释 Read View 的构成:只知道“RC 每次都创建快照,RR 只创建一次”,但当被追问“快照是什么,如何判断数据可见性”时,无法回答 Read View 的 m_ids, min_trx_id, max_trx_id 等内部细节和判断逻辑。这表明理解停留在表面。

工程实践与优化

  • 隔离级别的选择:MySQL 默认隔离级别是 RR,主要是为了保证主从复制(基于 statement 的 binlog)的数据一致性。但在现代基于 row 格式的 binlog 复制下,这个限制已不存在。许多互联网公司(如阿里)的核心业务数据库会主动将默认隔离级别设置为 RC。
    • 为什么用 RC:在 RC 级别下,事务每次 SELECT 都能读到已提交的最新数据,符合大多数业务直觉。更重要的是,RC 下不存在间隙锁(Gap Lock),只对扫描到的行加记录锁(Record Lock),这能极大减少锁冲突,提高并发性能。
    • 什么时候用 RR:当业务逻辑强依赖“事务期间数据快照必须保持绝对一致”时,例如需要进行一系列复杂计算和数据核对的后台批量任务,RR 提供了更强的一致性保证。
  • RR 级别下的长事务风险:在 RR 级别下,一个长事务(尤其是只读长事务)会导致其创建的 Read View 非常老。这会阻止 purge 线程回收该 Read View 创建之后产生的大量 undo log,可能导致 undo 表空间急剧膨胀,耗尽磁盘空间,并严重影响数据库性能。
    • 监控:应通过 information_schema.innodb_trx 监控长事务,并及时处理。
    • 优化:将长查询、数据分析、批量导出等任务放到只读从库执行,或在业务低峰期执行,避免在主库上产生长事务。
  • 执行计划分析:EXPLAIN 无法直接展示 Read View 的行为。但理解 Read View 机制有助于你推断在并发场景下,为何一条看起来走了索引的简单 SELECT 语句返回的是“旧”数据,从而避免误判为是索引或 SQL 的问题。

常见追问与延伸题目

  1. 追问:RR 是如何解决幻读的?为什么又说它没有“完全”解决?

    • 回答要点 1 (快照读):对于普通 SELECT 语句(快照读),RR 通过复用事务开始时的 Read View 来解决幻读。因为 Read View 是固定的,在事务期间其他事务所插入并提交的新行,其 DB_TRX_ID 会大于等于 Read View 的 max_trx_id,因此对当前事务是不可见的。
    • 回答要点 2 (当前读):对于 SELECT ... FOR UPDATE 或 UPDATE 等当前读操作,MVCC 不起作用,必须通过加锁来保证。RR 使用 Next-Key Lock(行锁 + 间隙锁)来锁定一个范围,防止其他事务在这个范围内插入新数据,从而避免了当前读场景下的幻读。
    • 回答要点 3 (未完全解决的场景):在某些极端情况下,例如一个事务先执行了快照读,然后基于读取的结果去执行当前读(如 UPDATE),可能会出现数据不一致。但最经典的“未完全解决”是指,如果没有正确使用锁,或者在特定并发序列下,应用层面仍可能感知到“幻象”。但从数据库层面,InnoDB 的 RR 已经通过 MVCC 和 Next-Key Lock 提供了非常强的幻读防护。
  2. 追问:一个在 RR 级别下的长事务会引发什么具体问题?如何排查和避免?

    • 回答要点 1 (问题):主要问题是 Undo Log 积压。由于长事务持有一个旧的 Read View,系统无法清理在该 Read View 之后产生的 Undo Log 版本链。这会导致:1) Undo 表空间持续增大,可能耗尽磁盘。2) 其他查询在需要访问旧版本数据时,需要遍历很长的版本链,导致查询性能下降。
    • 回答要点 2 (排查):可以通过查询 information_schema.innodb_trx 表来定位运行时间过长的事务。关注 trx_started 和 trx_state 字段,找出长时间处于 RUNNING 状态的事务及其执行的 SQL (trx_query)。
    • 回答要点 3 (避免):1) 业务拆分:避免在 OLTP 系统中执行长时间的数据分析或批量处理任务。2) 读写分离:将这类长查询任务迁移到只读从库。3) 设置超时:为事务设置合理的执行超时时间,例如使用 max_execution_time 参数(MySQL 8.0+)或应用层面的超时控制。4) 优化 SQL:确保事务内的 SQL 都是高效的,避免全表扫描等慢查询。
  3. 追问:如果我把隔离级别设为 RC,但某个业务场景又需要可重复读,该怎么办?

    • 回答要点 1 (悲观锁):在事务开始时,对需要读取和操作的数据行显式使用 SELECT ... FOR UPDATE。这会给这些行加上排他锁,阻止其他事务修改它们,直到当前事务提交。这样就以牺牲并发为代价,在 RC 级别手动实现了对特定数据的“可重复读”和“防更新”。
    • 回答要点 2 (乐观锁):在表中增加一个 version 字段。读取数据时一并读出 version 值。更新时,UPDATE 语句的 WHERE 条件中要包含 AND version = old_version。如果更新影响的行数为 0,说明数据已被其他事务修改,此时应用层面可以进行重试或抛出异常。
    • 回答要点 3 (临时设置):可以在事务级别动态修改隔离级别:SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;,执行需要 RR 保证的业务逻辑,然后 COMMIT。这样只在需要时才使用 RR,对系统的整体影响较小。