Skip to content

MySQL的MVCC,RR隔离级别下怎么解决幻读

约 2704 字大约 9 分钟

MySQL美团

2025-04-30

⭐ 题目日期:

美团 - 2025/4/25

📝 题解:

1. 概念解释

  • 事务 (Transaction): 数据库操作的最小逻辑单元,它包含了一系列数据库操作,这些操作要么全部成功执行,要么全部失败回滚,以确保数据库状态的一致性 (ACID特性中的原子性 Atomicity 和 一致性 Consistency)。
  • 事务隔离级别 (Transaction Isolation Level): 定义了多个并发事务之间相互影响的程度。SQL标准定义了四种隔离级别:读未提交 (Read Uncommitted)、读已提交 (Read Committed, RC)、可重复读 (Repeatable Read, RR)、串行化 (Serializable)。隔离级别越高,并发性能越低,但数据一致性越好。
  • 幻读 (Phantom Read): 是指在一个事务内,前后两次查询同⼀范围的数据,第二次查询看到了第⼀次查询未看到的⾏ (被其他已提交事务新插⼊的数据)。就像出现了“幻影”一样。注意它与不可重复读的区别:不可重复读是针对同一行数据的修改或删除,而幻读是针对一批数据(通常是范围查询)中新增了行。
    • 类比: 你第一次查询时,发现班级里有30个学生。在你查询后,另一个事务插入了一个新学生并提交了。你再次查询班级所有学生,发现变成了31个。这个多出来的学生就是“幻影”。
  • MVCC (Multi-Version Concurrency Control - 多版本并发控制): 是数据库用来解决并发访问问题的一种机制,尤其是在读多写少的场景下。它避免了简单的加锁方式带来的性能开销。其核心思想是:为每一行数据保留多个版本,每个事务访问时,数据库会根据事务的隔离级别和启动时间,选择一个合适的版本给它,从而实现读写不阻塞读读不阻塞。InnoDB存储引擎就采用了MVCC。

2. 解题思路

面试官问这个问题,核心是想考察你对InnoDB并发控制机制的理解,特别是MVCC如何与RR隔离级别协作来防止幻读。

核心答案: MySQL InnoDB在RR (Repeatable Read) 隔离级别下,主要通过 MVCC (结合Undo Log和Read View)Next-Key Lock (间隙锁+记录锁) 共同解决幻读问题。

  • 对于快照读 (Snapshot Read / Consistent Read),即普通的 SELECT 语句:

    1. MVCC机制生效:
      • Undo Log: InnoDB会为每行数据存储几个隐藏列,包括 DB_TRX_ID (最后修改该行的事务ID) 和 DB_ROLL_PTR (指向该行上一个版本的Undo Log记录)。当数据被修改时,旧版本数据会被存入Undo Log,形成一个版本链。
      • Read View (读视图): 当一个事务首次执行快照读时(在RR级别下),InnoDB会为该事务创建一个Read View。Read View本质上是当前数据库活跃事务(未提交的事务)的一个列表,以及一些边界信息。它决定了当前事务能看到哪些版本的数据。Read View包含几个关键部分:
        • m_ids: 创建Read View时,活跃(未提交)的事务ID列表。
        • min_trx_id: m_ids中的最小事务ID。
        • max_trx_id: 创建Read View时,系统下一个将要分配的事务ID(即大于所有已存在事务ID)。
        • creator_trx_id: 创建该Read View的事务ID。
      • 可见性判断规则: 当事务读取某行数据时,会比较该行记录的DB_TRX_ID和Read View:
        1. 如果 DB_TRX_ID < min_trx_id:说明修改该行的事务在当前事务创建Read View前已经提交,所以该版本数据可见
        2. 如果 DB_TRX_ID >= max_trx_id:说明修改该行的事务在当前事务创建Read View之后才开启,所以该版本数据不可见,需要沿着Undo Log链找上一个版本。
        3. 如果 min_trx_id <= DB_TRX_ID < max_trx_id
          • DB_TRX_IDm_ids 列表中:说明修改该行的事务在创建Read View时仍然活跃(未提交),所以该版本数据不可见,需要找上一个版本。
          • DB_TRX_ID 不在 m_ids 列表中:说明修改该行的事务在创建Read View时已经提交,所以该版本数据可见
        4. 如果 DB_TRX_ID == creator_trx_id:说明是当前事务自己修改的记录,可见
    2. RR隔离级别的关键: 在RR隔离级别下,事务的Read View是在第一次执行快照读时创建的,并且在整个事务期间都不会改变。这意味着,无论其他事务后续插入了多少新数据并提交,当前事务通过快照读查询时,总是基于事务开始时的那个“快照”(由固定的Read View定义),因此看不到其他事务新插入的行,从而避免了幻读。
  • 对于当前读 (Current Read),即 SELECT ... FOR UPDATE, SELECT ... LOCK IN SHARE MODE, INSERT, UPDATE, DELETE 语句:

    1. Next-Key Lock (临键锁): MVCC本身只解决快照读的幻读问题。对于当前读操作(需要读取最新已提交版本并可能加锁),InnoDB使用Next-Key Lock来防止幻读。
    2. 机制: Next-Key Lock是记录锁 (Record Lock)间隙锁 (Gap Lock) 的组合。它不仅锁定匹配查询条件的记录本身,还锁定这些记录之间的间隙
      • 记录锁: 锁定索引记录。
      • 间隙锁: 锁定索引记录之间的开区间,或者第一个索引记录之前的区间,或最后一个索引记录之后的区间。间隙锁的目的是阻止其他事务在这个间隙中插入新的记录
    3. 效果: 当一个事务执行当前读并获取了Next-Key Lock时,其他事务无法在该范围内插入任何新的记录,直到持有锁的事务提交或回滚。这样就阻止了幻读的发生。
    • 总结当前读: 通过Next-Key Lock,InnoDB在RR级别下锁定了读取的范围(包括间隙),阻止了其他事务的插入操作,从而避免了当前读场景下的幻读。

3. 知识扩展

  • 不同隔离级别的幻读情况:
    • Read Uncommitted: 存在脏读、不可重复读、幻读。
    • Read Committed (RC): 解决了脏读。每次快照读都创建新的Read View,所以存在不可重复读和幻读。
    • Repeatable Read (RR): 解决了脏读、不可重复读。通过MVCC (快照读) 和 Next-Key Lock (当前读) 基本解决了幻读。但在某些特殊情况下(如下文陷阱所述)仍需注意。
    • Serializable: 解决了所有并发问题,包括幻读。通常通过给所有读操作加锁实现,并发性能最低。
  • 其他并发问题:
    • 脏读 (Dirty Read): 一个事务读取了另一个事务未提交的数据。
    • 不可重复读 (Non-Repeatable Read): 同一个事务内,两次读取同一行数据,得到的结果不同(因为中间被其他事务修改或删除并提交了)。
  • Undo Log 的其他作用: 除了支持MVCC,Undo Log也用于事务的回滚 (Rollback)。
  • Gap Lock 的副作用: 间隙锁虽然解决了幻读,但也可能降低并发度,因为它锁定的范围比实际数据行要大。特定场景下可能导致死锁。可以通过调整隔离级别为RC(如果业务允许)或优化SQL来缓解。
  • 快照读 vs 当前读: 理解这两者的区别对于深入理解并发控制至关重要。普通SELECT是快照读,依赖MVCC;加锁读 (SELECT ... FOR UPDATE/LOCK IN SHARE MODE) 和 DML (INSERT/UPDATE/DELETE) 是当前读,依赖锁机制(包括Next-Key Lock)。

4. 实际应用

  • 为什么MySQL默认RR? 这是性能和一致性的一个折衷。相比Serializable,RR提供了更好的并发性能;相比RC,RR提供了更好的数据一致性保证(可重复读和基本解决幻读),这对于很多业务场景(如需要在一个事务中多次查询并保持数据视图一致的报表生成、账务处理等)非常重要。尤其在早期主从复制基于Statement格式时,RR能保证主从数据的一致性。
  • 何时考虑RC? 如果业务场景对幻读不敏感,且希望获得更高的并发性能(因为RC下的Gap Lock使用较少),可以考虑将隔离级别设置为Read Committed。很多互联网应用的核心交易链路之外的读服务可能会选择RC。
  • 复杂事务中的幻读风险: 在一个长事务中,如果混合使用了快照读和当前读,或者事务逻辑复杂,开发者需要特别注意数据的一致性。例如,先执行了一次快照读,然后在事务中执行了INSERTUPDATE(当前读会更新Read View或加锁),再执行快照读,可能会观察到“不一致”的现象(并非严格意义的幻读,而是自身操作导致的数据变化)。

5. 常见陷阱

  • 误区1:认为MVCC解决了所有幻读。 MVCC主要解决的是快照读场景下的幻读。对于当前读,需要依赖Next-Key Lock来解决。面试时要能清晰区分这两种情况。
  • 误区2:不理解RR下Read View的创建时机。 必须强调RR是在事务中第一次快照读时创建Read View,并且复用。而RC是每次快照读都创建新的Read View。这是两者在MVCC行为上的核心区别。
  • 误区3:混淆不可重复读和幻读。 清晰解释两者的区别:前者是行数据内容变了或行消失了,后者是行的数量变了(通常是增加了)。
  • 误区4:认为RR完全杜绝了幻读。 虽然RR能解决绝大部分幻读场景,但在一些极端或特定操作序列下,比如在一个事务中先快照读,然后更新了某个范围的数据(触发当前读和加锁),之后再快照读,可能会看到自己更新带来的“幻象”。但这通常被认为是符合隔离级别定义的行为。关键在于理解快照读和当前读的不同表现。面试官有时会追问这种边界情况。
  • 误区5:忽略Next-Key Lock是Record Lock + Gap Lock的组合。 要能解释清楚Gap Lock的作用是阻止插入。