MySQL事务与并发之:`事务`的`多版本并发控制`(`MVCC`):`Read View`的底层实现。

MySQL事务与并发:MVCC之Read View底层实现

各位同学,大家好!今天我们来深入探讨MySQL事务并发控制中的一个核心概念——MVCC (Multi-Version Concurrency Control),特别是其中的Read View。MVCC是MySQL InnoDB引擎实现高并发的关键技术,它允许多个事务同时读取数据,而无需互相阻塞,从而提高了系统的整体性能。我们将会重点关注Read View的底层实现,以及它如何影响事务读取数据的方式。

1. MVCC 简介

在理解Read View之前,我们需要先了解MVCC的基本概念。MVCC的核心思想是:对数据库中的每一行数据,维护多个版本,每个版本对应一个事务。当事务读取数据时,它会根据一定的规则选择一个合适的版本进行读取,而不是直接读取最新的版本。这样,读取操作就不会阻塞写入操作,写入操作也不会阻塞读取操作,从而实现了并发访问。

InnoDB实现MVCC主要依赖以下几个关键技术:

  • 隐藏字段: InnoDB为每一行数据添加了三个隐藏字段:
    • DB_TRX_ID: 创建或最后一次修改该行的事务ID。
    • DB_ROLL_PTR: 指向undo log的回滚指针。
    • DB_ROW_ID: 如果没有定义主键,InnoDB会自动创建一个ROW_ID作为主键。
  • Undo Log: 用于记录事务修改之前的数据状态。当事务需要回滚时,可以通过Undo Log恢复到之前的版本。Undo Log分为Insert Undo Log和Update Undo Log。Insert Undo Log是在插入数据时产生的,用于回滚插入操作。Update Undo Log是在更新数据时产生的,用于回滚更新操作。
  • Read View: 用于判断事务能够看到哪些版本的数据。Read View是MVCC实现可见性控制的关键。

2. Read View 的作用

Read View 的主要作用是判断当前事务能够看到哪些版本的数据。换句话说,Read View定义了当前事务的可见性范围。当事务需要读取一行数据时,InnoDB会使用Read View来判断该行数据的哪个版本对当前事务可见。

3. Read View 的结构

Read View 是一个数据结构,它包含以下几个重要的成员:

  • trx_id: 当前事务的ID。
  • m_ids: 当前活跃事务ID的集合。这是一个重要的集合,它包含了创建Read View时所有未提交的事务ID。
  • min_trx_id: m_ids 集合中最小的事务ID。
  • max_trx_id: 下一个要分配的事务ID,也就是说,它是大于所有已分配事务ID的值。

4. Read View 的创建时机

Read View 的创建时机取决于事务的隔离级别。在MySQL中,有两种隔离级别会用到MVCC:

  • READ COMMITTED (RC): 在每次读取数据时都会创建一个新的Read View。这意味着,同一个事务在不同的时刻可能会看到不同的数据,因为在两次读取之间,可能有其他事务提交了新的数据。
  • REPEATABLE READ (RR): 在事务第一次读取数据时创建一个Read View,并且在整个事务期间都使用同一个Read View。这意味着,同一个事务在整个事务期间都会看到相同的数据,即使在读取之间,有其他事务提交了新的数据。这是MySQL InnoDB的默认隔离级别。

5. Read View 的可见性判断规则

当事务需要读取一行数据时,InnoDB会使用Read View来判断该行数据的哪个版本对当前事务可见。可见性判断规则如下:

  1. 如果数据的 DB_TRX_ID 等于当前事务ID(trx_id),说明数据是当前事务自己修改的,总是可见的。
  2. 如果数据的 DB_TRX_ID 小于 min_trx_id,说明数据是在创建Read View之前就已经提交的事务修改的,总是可见的。
  3. 如果数据的 DB_TRX_ID 大于等于 max_trx_id,说明数据是在创建Read View之后才启动的事务修改的,总是不可见的。
  4. 如果数据的 DB_TRX_ID 位于 min_trx_idmax_trx_id 之间,需要判断 DB_TRX_ID 是否在 m_ids 集合中:
    • 如果在 m_ids 集合中,说明数据是在创建Read View时,还未提交的活跃事务修改的,不可见。
    • 如果不在 m_ids 集合中,说明数据是在创建Read View时,已经提交的事务修改的,可见。

6. Read View 的底层实现(伪代码)

为了更好地理解Read View的底层实现,我们可以用伪代码来模拟可见性判断的过程。

bool is_visible(trx_id_t trx_id, const ReadView& read_view) {
  // 1. 当前事务自己修改的
  if (trx_id == read_view.trx_id) {
    return true;
  }

  // 2. 早于ReadView创建的事务
  if (trx_id < read_view.min_trx_id) {
    return true;
  }

  // 3. 晚于ReadView创建的事务
  if (trx_id >= read_view.max_trx_id) {
    return false;
  }

  // 4. 在ReadView创建时活跃的事务
  if (read_view.m_ids.contains(trx_id)) {
    return false;
  }

  // 5. 在ReadView创建时已提交的事务
  return true;
}

// 示例:
struct ReadView {
  trx_id_t trx_id;
  std::set<trx_id_t> m_ids;
  trx_id_t min_trx_id;
  trx_id_t max_trx_id;
};

int main() {
    //假设当前事务ID是100
    ReadView read_view;
    read_view.trx_id = 100;
    read_view.m_ids = {101, 102, 103}; // 创建ReadView时,事务101,102,103是活跃的
    read_view.min_trx_id = 90;
    read_view.max_trx_id = 110;

    // 测试不同事务ID的可见性
    std::cout << "Transaction 95 is visible: " << is_visible(95, read_view) << std::endl; // true
    std::cout << "Transaction 100 is visible: " << is_visible(100, read_view) << std::endl; // true
    std::cout << "Transaction 101 is visible: " << is_visible(101, read_view) << std::endl; // false
    std::cout << "Transaction 105 is visible: " << is_visible(105, read_view) << std::endl; // true
    std::cout << "Transaction 110 is visible: " << is_visible(110, read_view) << std::endl; // false

    return 0;
}

这个伪代码演示了Read View如何根据事务ID和Read View的成员来判断数据的可见性。

7. 实例分析

为了更好地理解Read View的作用,我们来看一个实例。假设我们有如下的事务执行序列:

时间 事务ID 操作 数据行(初始值:10) DB_TRX_ID DB_ROLL_PTR
T1 10 START
T2 20 START
T3 10 SELECT 10 0 NULL
T4 20 UPDATE 20 20 PTR1
T5 10 SELECT 10 (RR)/20 (RC) 0/20 NULL/PTR1
T6 20 COMMIT
T7 10 SELECT 10 (RR)/20 (RC) 0/20 NULL/PTR1
T8 10 COMMIT
  • T1: 事务10启动。
  • T2: 事务20启动。
  • T3: 事务10执行SELECT操作。此时,事务10创建一个Read View,假设trx_id = 10m_ids = {20}min_trx_id = 10max_trx_id = 21。根据Read View的可见性判断规则,事务10可以看到初始版本的数据(DB_TRX_ID = 0 < min_trx_id)。
  • T4: 事务20执行UPDATE操作,将数据行更新为20。此时,数据的DB_TRX_ID变为20,DB_ROLL_PTR指向Undo Log中的旧版本数据。
  • T5: 事务10再次执行SELECT操作。
    • READ COMMITTED: 事务10会创建一个新的Read View。假设此时m_ids为空,min_trx_id为21,max_trx_id 大于21。根据可见性判断规则,事务10可以看到事务20修改后的数据(DB_TRX_ID = 20 < min_trx_id)。
    • REPEATABLE READ: 事务10仍然使用第一次SELECT操作时创建的Read View。根据可见性判断规则,事务10无法看到事务20修改后的数据(min_trx_id <= DB_TRX_ID = 20 < max_trx_id && 20 在 m_ids中)。
  • T6: 事务20提交。
  • T7: 事务10再次执行SELECT操作。
    • READ COMMITTED: 事务10会创建一个新的Read View,由于20已经提交,所以可能看到20
    • REPEATABLE READ: 事务10仍然使用第一次SELECT操作时创建的Read View,所以仍然看不到事务20修改后的数据。
  • T8: 事务10提交。

这个例子清晰地展示了在不同的隔离级别下,Read View如何影响事务读取数据的方式,以及如何保证事务的一致性和隔离性。

8. Undo Log 与版本链

正如前面提到的,Undo Log在MVCC中扮演着至关重要的角色。它不仅用于事务的回滚,还用于构建数据的版本链。版本链是指同一行数据的多个版本,通过DB_ROLL_PTR指针连接起来形成的一个链表。当事务需要读取历史版本的数据时,InnoDB会根据Read View的可见性判断规则,沿着版本链找到合适的版本进行读取。

9. 源码分析(InnoDB)

Read View 的具体实现在 InnoDB 源码中比较复杂,涉及多个模块的交互。但是,我们可以从一些关键的数据结构和函数入手,了解其核心实现。

首先,ReadView 类定义了 Read View 的结构,其中包含了前面提到的 trx_idm_idsmin_trx_idmax_trx_id 等成员。

其次,trx_read_view_t 结构体是更底层的 Read View 实现,它与事务上下文相关联。

最后,row_mysql_handle_errors 函数是 InnoDB 中用于处理行数据的函数,它会根据 Read View 的可见性判断规则,选择合适的版本进行读取。

由于InnoDB源码过于庞大,这里只能提供一些关键的入口点,无法深入到每一行代码。建议大家结合InnoDB的源码,深入研究Read View的实现细节。

10. Read View 的优势与局限性

MVCC 和 Read View 带来了许多优势:

  • 提高并发性能: 允许多个事务同时读取数据,而无需互相阻塞,从而提高了系统的整体性能。
  • 提供一致性: 通过Read View的可见性判断规则,保证了事务在不同的隔离级别下,能够看到一致的数据。

但是,MVCC 也存在一些局限性:

  • 存储空间占用: 需要维护多个版本的数据,增加了存储空间的占用。
  • 版本链维护: 需要定期清理过期的版本,以避免版本链过长,影响性能。这个清理工作由InnoDB的purge线程负责。

11. 总结:Read View是MVCC可见性控制的核心

Read View是MySQL InnoDB引擎实现MVCC的关键技术,它通过定义事务的可见性范围,实现了并发访问和数据一致性。理解Read View的底层实现,有助于我们更好地理解MySQL的事务并发控制机制,从而更好地优化数据库应用。Read View的结构和可见性判断规则是理解MVCC的关键,需要深入掌握。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注