`InnoDB`的`MVCC`(`多版本并发控制`):`Read View`的`创建`与`可见性`判断`算法`。

InnoDB MVCC:Read View 的创建与可见性判断算法

大家好,今天我们来深入探讨 InnoDB 存储引擎中的 MVCC(多版本并发控制)机制,特别是 Read View 的创建以及基于 Read View 的可见性判断算法。 MVCC 是 InnoDB 实现并发控制的关键技术,它允许多个事务并发读写数据,而无需加锁,从而显著提升数据库的并发性能。

1. MVCC 简介

在深入 Read View 之前,我们先简单回顾一下 MVCC 的基本概念。 MVCC 的核心思想是:

  • 为每一行数据保存多个版本: 当一个事务修改某行数据时,InnoDB 并不会直接覆盖旧版本的数据,而是创建一个新的版本。
  • 通过 Read View 来判断事务可以访问哪些版本的数据: 每个事务在执行期间都会创建一个 Read View,它定义了当前事务可以看到哪些数据版本。

这样,不同的事务在不同的时间点创建 Read View,就可以看到不同版本的数据,从而实现并发读写而不相互阻塞。

2. Read View 的结构

Read View 是 MVCC 实现的关键数据结构,它包含了当前活跃事务的信息,用于判断数据版本的可见性。 在 InnoDB 中,Read View 主要包含以下几个关键属性:

  • trx_id: 创建该 Read View 的事务 ID。
  • m_ids: 当前活跃事务 ID 的集合。 这个集合包含了所有在创建 Read View 时,还未提交的事务 ID。
  • min_trx_id: m_ids 集合中最小的事务 ID。
  • max_trx_id: 下一个要分配的事务 ID。 也就是说,所有大于或等于 max_trx_id 的事务 ID,都是在创建 Read View 之后才启动的。

可以用表格来更清晰地呈现 Read View 的结构:

属性 描述
trx_id 创建该 Read View 的事务 ID。 当前事务本身的 ID。
m_ids 当前活跃事务 ID 的集合。 在创建 Read View 的那一刻,所有未提交的事务 ID 的集合。
min_trx_id m_ids 集合中最小的事务 ID。 小于 min_trx_id 的事务 ID,表示在创建 Read View 之前就已经提交的事务。
max_trx_id 下一个要分配的事务 ID。 大于或等于 max_trx_id 的事务 ID,表示在创建 Read View 之后才启动的事务。 max_trx_id 并不是 m_ids 集合中的最大值,而是系统下一个要分配的事务 ID。

3. Read View 的创建时机

Read View 的创建时机非常重要,它直接影响了事务能够看到的数据版本。 在 InnoDB 中,Read View 的创建时机取决于事务的隔离级别。

3.1 READ COMMITTED 隔离级别

在 READ COMMITTED 隔离级别下,每次读取数据时都会创建一个新的 Read View。 也就是说,每次执行 SELECT 语句时,都会重新生成一个 Read View。 这样,事务只能看到已经提交的数据,并且每次读取到的数据版本都可能是不同的。

3.2 REPEATABLE READ 隔离级别

在 REPEATABLE READ 隔离级别下,事务在第一次读取数据时创建一个 Read View,后续的读取都使用同一个 Read View。 也就是说,在整个事务期间,Read View 只创建一次。 这样,事务可以保证在整个事务期间读取到的数据版本是一致的,实现了可重复读。

4. 可见性判断算法

有了 Read View,就可以判断某个数据版本对当前事务是否可见。 InnoDB 使用以下算法来判断数据版本的可见性:

假设要判断的数据版本的事务 ID 为 trx_id_data

  1. 如果 trx_id_data 等于 trx_id,则该版本可见。

    这意味着数据版本是由当前事务创建的,因此肯定可见。

  2. 如果 trx_id_data 小于 min_trx_id,则该版本可见。

    这意味着数据版本是由在创建 Read View 之前就已经提交的事务创建的,因此对所有事务都可见。

  3. 如果 trx_id_data 大于或等于 max_trx_id,则该版本不可见。

    这意味着数据版本是由在创建 Read View 之后才启动的事务创建的,因此对当前事务不可见。

  4. 如果 trx_id_datamin_trx_idmax_trx_id 之间,则需要检查 trx_id_data 是否在 m_ids 集合中。

    • 如果在 m_ids 集合中,则该版本不可见。 这意味着数据版本是由在创建 Read View 时还未提交的活跃事务创建的,因此对当前事务不可见。
    • 如果不在 m_ids 集合中,则该版本可见。 这意味着数据版本是由在创建 Read View 时已经提交的事务创建的,因此对当前事务可见。

我们可以用伪代码来表示这个可见性判断算法:

def is_visible(trx_id_data, read_view):
  """
  判断数据版本是否对当前事务可见。

  Args:
    trx_id_data: 数据版本的事务 ID。
    read_view: 当前事务的 Read View。

  Returns:
    True if the data version is visible, False otherwise.
  """

  if trx_id_data == read_view.trx_id:
    return True  # 当前事务创建的版本,可见

  if trx_id_data < read_view.min_trx_id:
    return True  # 在 Read View 创建之前提交的事务创建的版本,可见

  if trx_id_data >= read_view.max_trx_id:
    return False # 在 Read View 创建之后启动的事务创建的版本,不可见

  if trx_id_data in read_view.m_ids:
    return False # 在 Read View 创建时未提交的活跃事务创建的版本,不可见

  return True # 在 Read View 创建时已经提交的事务创建的版本,可见

5. 举例说明

为了更好地理解 Read View 的创建和可见性判断算法,我们来看几个例子。

例子 1:READ COMMITTED 隔离级别

假设有三个事务 A、B 和 C,它们的事务 ID 分别为 10、11 和 12。 初始时,某行数据的值为 ‘X’,由事务 ID 为 5 的事务创建。

  1. 事务 A 启动,读取该行数据。 因为是 READ COMMITTED 隔离级别,所以创建一个新的 Read View。 假设此时活跃事务集合 m_ids 为空,min_trx_id 为 13 (因为 10,11,12都还在进行中,实际的实现应该更复杂,这里简化),max_trx_id 为 13(下一个分配的事务ID)。

    • Read View: trx_id = 10, m_ids = [], min_trx_id = 13, max_trx_id = 13
    • 要判断版本号为 5 的数据是否可见。 因为 5 < 13,所以该版本可见,事务 A 读取到 ‘X’。
  2. 事务 B 修改该行数据,将值改为 ‘Y’,并提交。 此时,该行数据有两个版本,一个版本的值为 ‘X’,事务 ID 为 5,另一个版本的值为 ‘Y’,事务 ID 为 11。

  3. 事务 A 再次读取该行数据。 因为是 READ COMMITTED 隔离级别,所以创建一个新的 Read View。 假设此时活跃事务集合 m_ids 为空,min_trx_id 为 13, max_trx_id 为 13。

    • Read View: trx_id = 10, m_ids = [], min_trx_id = 13, max_trx_id = 13
    • 首先判断版本号为 11 的数据是否可见。 因为 11 < 13,所以该版本可见,事务 A 读取到 ‘Y’。

例子 2:REPEATABLE READ 隔离级别

假设有三个事务 A、B 和 C,它们的事务 ID 分别为 10、11 和 12。 初始时,某行数据的值为 ‘X’,由事务 ID 为 5 的事务创建。

  1. 事务 A 启动,读取该行数据。 因为是 REPEATABLE READ 隔离级别,所以创建一个新的 Read View。 假设此时活跃事务集合 m_ids 为 [11, 12],min_trx_id 为 11, max_trx_id 为 13。

    • Read View: trx_id = 10, m_ids = [11, 12], min_trx_id = 11, max_trx_id = 13
    • 要判断版本号为 5 的数据是否可见。 因为 5 < 11,所以该版本可见,事务 A 读取到 ‘X’。
  2. 事务 B 修改该行数据,将值改为 ‘Y’,并提交。 此时,该行数据有两个版本,一个版本的值为 ‘X’,事务 ID 为 5,另一个版本的值为 ‘Y’,事务 ID 为 11。

  3. 事务 A 再次读取该行数据。 因为是 REPEATABLE READ 隔离级别,所以使用第一次读取时创建的 Read View

    • Read View: trx_id = 10, m_ids = [11, 12], min_trx_id = 11, max_trx_id = 13
    • 首先判断版本号为 11 的数据是否可见。 因为 11 在 m_ids 集合中,所以该版本不可见。
    • 然后判断版本号为 5 的数据是否可见。 因为 5 < 11,所以该版本可见,事务 A 仍然读取到 ‘X’。

通过这两个例子,我们可以看到,不同的隔离级别下,Read View 的创建时机和可见性判断算法会影响事务读取到的数据版本,从而实现不同的并发控制效果。

6. 源码分析 (简化)

虽然完整的源码分析非常复杂,但我们可以从宏观的角度了解 Read View 在 InnoDB 源码中的体现。 在 InnoDB 中,Read View 的实现类是 ReadView。它位于 storage/innobase/read/read0types.h 文件中(版本可能因 MySQL 版本而异)。

// (Simplified representation)
class ReadView {
public:
  trx_id_t m_trx_id;       // Transaction id of the read view creator
  trx_id_t m_low_limit_no;   // m_ids < this value are VISIBLE
  trx_id_t m_up_limit_no;    // m_ids >= this value are NOT VISIBLE
  ids_t   *m_ids;           // Set of active transaction ids at read view creation time
  // ... other members and methods ...
};

可见性判断算法则体现在 ReadView::changes_visible() 方法中 (也是简化版本)。

// (Simplified representation)
bool ReadView::changes_visible(
    trx_id_t   id,            /*!< in: transaction id to check */
    const table_id_t&,        /*!< in: table id; not used */
    const rec_t*    rec) const /*!< in: row being read; not used */
{
  ut_ad(id > 0);

  if (id < m_low_limit_no) {
    return(true);    // Created by committed trx before m_low_limit_no
  }

  if (id >= m_up_limit_no) {
    return(false);   // Created by trx started after m_up_limit_no
  }

  if (id == m_trx_id) {
    return(true);    // Created by this transaction.
  }

  // Check whether trx id is in m_ids
  return(!m_ids->member(id)); //If not a member, then it has committed.
}

这段代码与我们之前的伪代码描述的逻辑基本一致。 它根据事务 ID 和 Read View 的属性,判断数据版本是否可见。 注意,实际的 InnoDB 代码要复杂得多,包含了各种优化和边界情况处理。

7. 总结:Read View 在 MVCC 中扮演的角色

我们深入探讨了 InnoDB 中 MVCC 的核心组件——Read View。 Read View 通过记录创建时的活跃事务信息,为事务提供了一致性的数据快照。 不同的隔离级别决定了 Read View 的创建时机,而可见性判断算法则根据 Read View 的内容,判断数据版本的可见性。 理解 Read View 的创建和可见性判断算法,对于深入理解 InnoDB 的并发控制机制至关重要。

发表回复

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