那些年背过的题:MySQL MVCC实现原理深入分析

1,065 阅读9分钟

MySQL 的多版本并发控制(MVCC)是一种用于处理并发事务的机制,主要用于提高数据库的性能和一致性。InnoDB 是 MySQL 中最常用的存储引擎之一,它实现了 MVCC。

MVCC 的基本原理

  1. 数据版本化:在 MVCC 中,每行记录都有多个版本。每当有事务对数据进行修改时,不是直接覆盖旧的数据,而是创建一个新的版本。这些版本包含了必要的元数据信息,比如创建时间、过期时间等。

  2. 快照读:大多数 SELECT 查询都是使用快照读,这意味着查询会读取到某个时间点的一致性视图。通过这种方式,读操作不需要加锁,可以极大地提高并发性能

  3. 当前读:对于更新操作,InnoDB 使用当前读,即总是读取最新版本的数据并对其进行加锁以避免并发问题。

  4. 隐藏列:InnoDB 表中有两个隐藏的列,用于 MVCC:

    • trx_id: 记录最近一次插入或更新该行的事务 ID。
    • roll_pointer: 指向撤销日志的指针,用于恢复旧版本数据。
  5. 撤销日志(Undo Log) :存储之前版本的数据,以便在需要时可以执行回滚,也为快照读提供支持。

  6. 事务隔离级别:MVCC 支持多种事务隔离级别,常见的是 REPEATABLE READ。在这个级别下,事务开始时创建的快照在整个事务期间保持不变。

MVCC 快照的具体内容

  1. 事务 ID 列表

    • 活跃事务列表:在快照创建时,正在运行的所有事务的 ID 列表。通过这个列表,InnoDB 可以知道哪些事务是活跃的,从而判断当前事务可见的版本。
    • 最小事务 ID (min_trx_id):在活跃事务列表中最小的事务 ID,即这些事务在快照创建之前已经启动。
    • 最大事务 ID (max_trx_id):通常设定为系统中下一个将要分配的事务 ID,用于标识快照中的“未来”部分。
  2. 可见性规则

    使用快照时,InnoDB 依据以下规则判断一个版本是否对当前事务可见:

    • 如果数据版本的 trx_id 小于 min_trx_id,则表示该版本是在快照创建前已经提交的,因此对当前事务可见。

    • 如果数据版本的 trx_id 大于或等于 max_trx_id,表示该版本在快照创建后生成,对当前事务不可见。

    • 如果数据版本的 trx_id 介于 min_trx_id 和 max_trx_id 之间,则需要进一步检查:

      • 如果 trx_id 属于活跃事务列表,说明该事务尚未提交,因此该版本对当前事务不可见。
      • 如果 trx_id 不在活跃事务列表中,则该版本对当前事务可见。
  3. 版本链及回滚指针

    • 每条记录都有多个版本,通过 roll_pointer 链接成一个版本链。
    • 当读取一个记录时,InnoDB 会沿着版本链查找第一个符合可见性规则的版本。

MVCC 执行流程

在 MySQL 的 InnoDB 存储引擎中,事务隔离级别为 REPEATABLE READ 时,MVCC 的实现流程主要涉及如何管理并发事务的读取视图和数据版本。以下是详细流程:

1. 事务启动

当一个事务以 REPEATABLE READ 隔离级别启动时,InnoDB 会创建一个一致性视图(Consistent Read View),这个视图用于确保在整个事务过程中,读操作能看到的是事务启动时刻的一致性数据状态。

2. 快照读

  • 读取视图:在事务开始时创建的读取视图会捕获当前数据库的状态,包括已提交事务和正在进行中的事务。
  • 版本链:每个表行记录都有一个隐藏列 trx_id 和一个回滚指针 roll_pointer。通过这些信息,InnoDB 可以遍历数据版本链,从而找到符合当前事务可见性的版本。
  • 可见性判断:当事务要读取某一行数据时,通过 trx_id 和读取视图来判断该版本是否对当前事务可见。如果不可见,则沿着 roll_pointer 找到上一个版本进行同样的判断,直到找到一个可见版本或无版本可用。

3. 当前读

  • 当事务执行更新、删除或插入操作时,会进行当前读。这种情况下,InnoDB 会锁住最新版本的数据,并确保其他事务不会同时修改这部分数据。

4. 版本控制

  • 插入操作:新插入的行附带当前事务 trx_id 作为其版本。
  • 更新操作:生成一个新版本,旧版本保持不变并通过 roll_pointer 保持链接。
  • 删除操作:标记当前版本为删除,新版本不会被创建。

5. 事务提交与回滚

  • 提交:事务提交后,其所做的更改对于之后启动的事务立即可见。
  • 回滚:如果事务回滚,根据 undo log 恢复数据到之前的状态。

6. 幻读处理

  • 在 REPEATABLE READ 下,InnoDB 通过 Next-Key Locking 技术处理幻读问题,它不仅锁定存在的行,还会锁住索引范围,防止插入导致幻影行出现。

Next-Key Locking 结合了行锁(Record Lock)和间隙锁(Gap Lock)。它不仅锁住了索引记录本身,还锁住了这些记录之间的空隙。

案例说明

假设有一个表 accounts,包含以下字段:id, name, 和 balance

初始状态

表中有一条记录:

idnamebalance
1Alice1000

版本链操作过程

  1. 事务 T1 开始

    • T1 开始时间戳为 10,读取 accounts

    • 当前数据版本:

      • trx_id: 9
      • 数据内容:(id=1, name='Alice', balance=1000)
  2. 事务 T2 开始并更新

    • T2 开始时间戳为 11,对 balance 进行更新:

      UPDATE accounts SET balance = 1200 WHERE id = 1;
      
    • 新数据版本(未提交):

      • trx_id: 11
      • 数据内容:(id=1, name='Alice', balance=1200)
    • 旧数据版本仍然存在,形成版本链。

  3. 事务 T3 开始并提交更新

    • T3 开始时间戳为 12,插入新记录:

      INSERT INTO accounts (id, name, balance) VALUES (2, 'Bob', 500);
      
    • 提交后,生成新的已提交版本。

  4. 事务 T1 查询

    • T1 再次查询 accounts

      SELECT * FROM accounts WHERE id = 1;
      
    • 可见性判断:

      • T1 的开始时间为 10,它只看得到 trx_id 小于等于 10 的版本。
      • 因此,T1 看到的是旧版本 (id=1, name='Alice', balance=1000)
  5. 事务 T2 提交

    • T2 提交时,更新的版本 (trx_id: 11) 成为有效版本。

    • 数据链变为:

      • 已提交版本:(id=1, name='Alice', balance=1200, trx_id: 11)
  6. 新事务 T4 开始并查询

    • T4 开始时间戳为 13,读取 accounts

      SELECT * FROM accounts WHERE id = 1;
      
    • 可见性判断:

      • T4 的开始时间为 13,它可以看到所有在时间戳 13 前提交的版本。
      • 于是,T4 看到最新的已提交版本 (id=1, name='Alice', balance=1200)

总结

  • 版本链:每次数据修改都会产生一个新版本,旧版本通过 roll_pointer 链接起来,形成版本链。
  • 可见性判断:每个事务只能看到其开始之前提交的版本,通过对比事务的开始时间戳与行的 trx_id 来决定可见性。

回滚流程

在 MySQL 的 InnoDB 存储引擎中,事务回滚操作会影响版本链的状态。回滚发生时,未提交的修改将被撤销,并且对最终的数据可见性不会产生影响。这是通过撤销日志(undo log)和版本链的管理实现的。

事务回滚过程中版本链的变化

场景描述

假设我们有一个表 accounts,其中包含以下记录:

idnamebalance
1Alice1000

操作步骤

  1. 事务 T1 开始

    • T1 对记录进行更新:

      UPDATE accounts SET balance = 1200 WHERE id = 1;
      
    • 此时,InnoDB 会为该记录生成一个新的未提交的版本。

    • 版本链:

      • 新版本(未提交):(id=1, name='Alice', balance=1200, trx_id=T1)
      • 旧版本:(id=1, name='Alice', balance=1000)
  2. 事务 T1 回滚

    • 在 T1 决定回滚的时候,系统会使用 undo log 将数据恢复到它执行更新之前的状态。

    • 未提交的新版本 (trx_id=T1) 被丢弃,不再存在于版本链中。

    • 旧版本成为当前有效版本:

      • 有效版本:(id=1, name='Alice', balance=1000)
  3. 回滚后的结果

    • 在 T1 回滚完成后,其他事务或查询看到的都是回滚前的状态,即 balance=1000 的记录。
    • 由于 T1 的更改从未真正被提交到数据库,所以不会有任何其他事务看到其更改过的版本。

总结

  • Undo Log:回滚时使用 undo log 将数据恢复到事务开始之前的状态。这是 MVCC 实现一致性快照和允许事务回滚的关键。
  • 版本链维护:当事务未提交时,它的更改仅对自己可见。回滚后这些更改被撤销,未提交的版本从版本链中移除。
  • 数据一致性:回滚保证了即使事务发生错误或被用户取消,数据库依然能保持一致的状态。

思考题:RR与RC性能对比

影响性能的因素

  1. 锁争用

    • 在高并发写操作下,REPEATABLE READ 由于使用间隙锁(gap locking),可能导致更多的锁争用,从而增加事务等待时间。
    • READ COMMITTED 由于仅锁定当前访问的行,因此在同等条件下通常能提供更好的并发性能。
  2. 应用场景

    • 对于需要稳定视图以确保业务逻辑正确性的场景(例如,复杂的报表或分析查询),REPEATABLE READ 可以避免因数据变化所产生的不一致结果。
    • 在实时性要求较高且允许轻微不一致的场景(例如,一些在线系统的浏览操作),READ COMMITTED 提供的灵活性和性能优势可能更明显。