跳转至

InnoDB存储引擎对MVCC的实现

MVCC (Multi-Version Concurrency Control) 的核心目的是为了实现“读写不冲突”。在不加锁的情况下,让读操作读取历史版本数据(快照读),而写操作修改最新版本数据。

MVCC 会定期进行版本的回收,以防止数据库中的版本无限增长。

  • MVCC 通过创建数据的多个版本和使用快照读取来实现并发控制。
  • 读操作使用旧版本数据的快照,写操作创建新版本,并确保原始版本仍然可用。
  • 这样,不同的事务可以在一定程度上并发执行,而不会相互干扰,从而提高了数据库的并发性能和数据一致性。

InnoDB 对 MVCC 的实现主要依赖于三个“隐式”组件:

  • 隐藏字段 (Hidden Columns)
  • Undo Log (回滚日志)
  • ReadView (读视图)

简单来说:Undo Log 保存了历史快照,隐藏字段将这些快照串联成链,而 ReadView 则充当裁判,决定当前事务能看到链条上的哪一个版本。

一、 实现机制详解 (The How)

  1. 隐藏字段 (Hidden Columns)
  2. InnoDB 在每行数据背后都默默维护了几个不可见的字段,最关键的是以下两个 :
    • DB_TRX_ID (6字节):最近一次修改(Insert/Update)该行数据的事务ID。
    • DB_ROLL_PTR (7字节):回滚指针。它指向 Undo Log 中该行的上一个版本。
  3. Undo Log (版本链)
  4. 当事务修改数据时,InnoDB 不会直接覆盖旧数据,而是先将旧数据拷贝到 Undo Log 中。
  5. 通过 DB_ROLL_PTR 指针,最新的数据行可以像链表一样,顺藤摸瓜找到之前的所有老版本。这就是版本链 。
  6. ReadView (读视图/可见性判断)
  7. ReadView 是事务进行快照读 (Snapshot Read) 时产生的“读视图”。它记录了生成时刻系统活跃事务的状态,用于判断版本链中的哪个版本对当前事务可见 。

ReadView 包含四个核心字段(变量名对应源码):

  • m_creator_trx_id:生成该 ReadView 的事务ID(即“我”的ID)。
  • m_ids:生成 ReadView 时,系统中当前活跃(未提交)的事务ID列表。
  • m_up_limit_id (min_trx_id):m_ids 中最小的事务ID。如果数据版本的事务ID小于它,说明那是很久以前提交的,肯定可见。
  • m_low_limit_id (max_trx_id):生成 ReadView 时,系统应该分配给下一个事务的ID(即最大事务ID + 1)。

二、 可见性算法 (The Why)

当你的事务读取一行数据时,InnoDB 会拿到该行数据的 DB_TRX_ID,拿着它去跟你的 ReadView 做比较。规则如下 :

比较规则 解释 结果
trx_id == creator_id 数据就是我自己改的 可见
trx_id < min_trx_id 数据在快照创建前已提交 可见
trx_id >= max_trx_id 数据是快照创建后才开启的事务改的 不可见
trx_id IN m_ids 数据是由此时还在活跃(未提交)的事务改的 不可见
trx_id NOT IN m_ids 虽在区间内,但不在活跃列表,说明已提交 可见

如果当前版本不可见,InnoDB 就会顺着 DB_ROLL_PTR 找到 Undo Log 中的下一个版本,重复上述判断,直到找到可见版本或链表结束。

三、 RC 与 RR 的区别

Read Committed (RC) 和 Repeatable Read (RR) 隔离级别的核心区别,仅仅在于生成 ReadView 的时机不同 。

  • RC (读已提交):每次执行 SELECT 语句时,都会重新生成一个新的 ReadView。
    • 结果:能读到其他事务刚刚提交的数据。
  • RR (可重复读):第一次执行 SELECT 语句时生成 ReadView,后续所有的 SELECT 都复用这个 ReadView。
    • 结果:不管其他事务怎么提交,我看到的永远是第一次读取时的快照(实现了可重复读)。

四、总结

  1. MVCC 利用 Undo Log 构建数据的历史版本,利用 ReadView 决定当前事务能看到哪个版本。
  2. 它主要解决了读写并发问题,让查询不用加锁,极大提高了数据库的并发性能。
  3. 当前读 (Current Read):注意,MVCC 只作用于普通 SELECT。如果你使用 SELECT ... FOR UPDATE 或 UPDATE/DELETE,那是当前读,会读取最新版本并加锁(Record Lock 或 Next-Key Lock),那就不走 MVCC 这一套快照逻辑了 。