MySQL的`Innodb`的`MVCC`:`Read View`是如何工作的?

InnoDB MVCC:Read View 的工作原理

大家好,今天我们来深入探讨 MySQL InnoDB 存储引擎中 MVCC(Multi-Version Concurrency Control,多版本并发控制)机制的关键组成部分:Read View。理解 Read View 的工作原理对于理解 InnoDB 的事务隔离级别和并发控制至关重要。

什么是 MVCC?

在深入 Read View 之前,我们先简单回顾一下 MVCC。MVCC 是一种并发控制方法,它允许多个事务同时读取和修改同一份数据,而不需要互相阻塞。每个事务在读取数据时,会看到一个数据在特定时间点的“快照”版本。当一个事务修改数据时,并不会立即覆盖原始数据,而是创建一个新的版本,并保留旧版本。这样,其他正在读取数据的事务仍然可以访问旧版本,从而避免了读写冲突。

Read View 的作用

Read View 是 MVCC 实现的核心概念之一。它本质上是事务在执行查询时创建的一个“一致性视图”,定义了该事务能“看到”哪些版本的数据。换句话说,Read View 决定了当前事务能够读取到的数据的版本。

Read View 的结构

Read View 主要包含以下几个关键字段:

  • trx_id: 当前事务的 ID。

  • creator_trx_id: 创建该 Read View 的事务 ID。通常情况下,creator_trx_id 等于 trx_id,但在某些特殊情况下(例如,使用 FOR UPDATE 锁定读取数据时),creator_trx_id 可能与 trx_id 不同。

  • up_limit_id: 小于该 ID 的事务已经提交,对于当前 Read View 可见。

  • low_limit_id: 大于等于该 ID 的事务尚未提交,对于当前 Read View 不可见。

  • trx_ids: 一个活跃事务 ID 的集合,包含了创建 Read View 时所有未提交的事务 ID。 trx_ids 中的事务 ID 既可能小于 up_limit_id,也可能大于 up_limit_id。如果一个事务 ID 存在于 trx_ids 中,则表示该事务在创建 Read View 时是活跃的。

可以将Read View的结构用下表展示:

字段名 描述
trx_id 当前事务的 ID。
creator_trx_id 创建 Read View 的事务 ID。
up_limit_id 小于该 ID 的事务已经提交,对当前 Read View 可见。
low_limit_id 大于等于该 ID 的事务尚未提交,对当前 Read View 不可见。
trx_ids 创建 Read View 时所有未提交的事务 ID 的集合。 trx_ids 中的事务 ID 既可能小于 up_limit_id,也可能大于 up_limit_id。如果一个事务 ID 存在于 trx_ids 中,则表示该事务在创建 Read View 时是活跃的。

Read View 的创建时机

Read View 的创建时机与事务的隔离级别密切相关。

  • READ COMMITTED(RC)隔离级别: 在 RC 隔离级别下,每个 语句 开始执行时都会创建一个新的 Read View。这意味着,同一个事务中的不同语句可能会看到不同的数据版本,因为在执行后续语句时,可能已经有其他事务提交了修改。

  • REPEATABLE READ(RR)隔离级别: 在 RR 隔离级别下,事务只在 第一次 执行 SELECT 语句时创建一个 Read View。后续的 SELECT 语句都会重用这个 Read View。这意味着,在同一个事务中,所有的 SELECT 语句都将看到相同的数据版本,直到事务结束。这是 MySQL InnoDB 的默认隔离级别。

Read View 的可见性判断规则

当事务要读取某一行数据时,InnoDB 会根据该行的 trx_id 和 Read View 中的信息来判断该版本的数据是否对当前事务可见。判断规则如下:

  1. 如果行的 trx_id 等于当前事务的 trx_id,则该版本的数据可见。 这是因为当前事务对该行进行了修改,理应能看到自己的修改。

  2. 如果行的 trx_id 小于 up_limit_id,则该版本的数据可见。 这意味着创建该版本的事务在创建 Read View 之前已经提交,所以该版本对当前事务可见。

  3. 如果行的 trx_id 大于等于 low_limit_id,则该版本的数据不可见。 这意味着创建该版本的事务在创建 Read View 之后才启动,或者在创建 Read View 时还未提交,所以该版本对当前事务不可见。

  4. 如果行的 trx_id 位于 up_limit_idlow_limit_id 之间,则需要检查 trx_id 是否存在于 trx_ids 集合中

    • 如果 trx_id 存在于 trx_ids 集合中,则该版本的数据不可见。 这意味着创建该版本的事务在创建 Read View 时还未提交,所以该版本对当前事务不可见。
    • 如果 trx_id 不存在于 trx_ids 集合中,则该版本的数据可见。 这意味着创建该版本的事务在创建 Read View 之前就已经提交,所以该版本对当前事务可见。

可以用下表总结可见性判断规则:

条件 结果 说明
row.trx_id == read_view.trx_id 可见 当前事务修改的数据,总是可见。
row.trx_id < read_view.up_limit_id 可见 创建该版本的事务在创建 Read View 之前已经提交。
row.trx_id >= read_view.low_limit_id 不可见 创建该版本的事务在创建 Read View 之后启动或创建时还未提交。
row.trx_id 在 [up_limit_id, low_limit_id) 之间 取决于trx_ids 如果 row.trx_id 存在于 read_view.trx_ids 中,则不可见(创建该版本的事务在创建 Read View 时未提交)。
如果 row.trx_id 不存在于 read_view.trx_ids 中,则可见(创建该版本的事务在创建 Read View 之前已经提交)。

示例说明

为了更好地理解 Read View 的工作原理,我们来看一个具体的例子。

假设有三个事务 A、B 和 C,它们的事务 ID 分别为 10、20 和 30。

  1. 初始状态: 表 users 中有一行数据,id = 1, name = 'Alice', trx_id = 5 (假设由事务ID为5的事务提交)

  2. 事务 A 启动 (trx_id = 10): 事务 A 启动,并执行一个 SELECT 语句。此时,InnoDB 会为事务 A 创建一个 Read View。假设当前系统中只有事务 A 处于活跃状态,那么事务 A 的 Read View 如下:

    • trx_id = 10
    • creator_trx_id = 10
    • up_limit_id = 11 (小于11的事务已经提交)
    • low_limit_id = MAX(trx_id) + 1 (假设MAX(trx_id)为当前最大的事务ID,这里假设为30,所以low_limit_id = 31)
    • trx_ids = {10}

    根据可见性判断规则,trx_id = 5 小于 up_limit_id = 11,所以事务 A 可以看到 id = 1, name = 'Alice' 这行数据。

  3. 事务 B 启动 (trx_id = 20): 事务 B 启动,并将 name 修改为 'Bob',然后提交。 此时,users表会新增一条数据版本,id = 1, name = 'Bob', trx_id = 20

  4. 事务 C 启动 (trx_id = 30): 事务 C 启动,并执行一个 SELECT 语句。此时,InnoDB 会为事务 C 创建一个 Read View。假设当前系统中只有事务 C 处于活跃状态,那么事务 C 的 Read View 如下:

    • trx_id = 30
    • creator_trx_id = 30
    • up_limit_id = 21 (小于21的事务已经提交)
    • low_limit_id = MAX(trx_id) + 1 (假设MAX(trx_id)为当前最大的事务ID,这里假设为30,所以low_limit_id = 31)
    • trx_ids = {30}

    根据可见性判断规则:

    • 对于 id = 1, name = 'Alice', trx_id = 5,由于 trx_id = 5 小于 up_limit_id = 21,所以事务 C 可以看到该版本的数据。
    • 对于 id = 1, name = 'Bob', trx_id = 20,由于 trx_id = 20 小于 up_limit_id = 21,所以事务 C 也可以看到该版本的数据。

    因此,事务 C 会看到 id = 1, name = 'Bob' 这行数据。

  5. 事务 A 再次执行 SELECT (假设隔离级别为RR):由于事务 A 的 Read View 在第一次 SELECT 时已经创建,所以它会继续使用之前的 Read View。

    • trx_id = 10
    • creator_trx_id = 10
    • up_limit_id = 11
    • low_limit_id = 31
    • trx_ids = {10}

    根据可见性判断规则:

    • 对于 id = 1, name = 'Alice', trx_id = 5,由于 trx_id = 5 小于 up_limit_id = 11,所以事务 A 可以看到该版本的数据。
    • 对于 id = 1, name = 'Bob', trx_id = 20,由于 trx_id = 20 大于 up_limit_id = 11,且 trx_id = 20 小于 low_limit_id = 31,需要判断 20 是否存在于 trx_ids = {10} 中。由于 20 不存在于 trx_ids 中,但是trx_id=20大于up_limit_id=11,所以事务A不可见。

    因此,事务 A 仍然会看到 id = 1, name = 'Alice' 这行数据,即使事务 B 已经提交了修改。这就是 RR 隔离级别下,事务 A 能够保持一致性视图的原因。

代码示例

虽然我们无法直接访问 InnoDB 内部的 Read View 结构,但我们可以通过一些技巧来观察 MVCC 的行为。

以下是一个使用 Python 和 MySQL Connector/Python 库来模拟 MVCC 行为的示例:

import mysql.connector

# 数据库连接配置
config = {
    'user': 'your_user',
    'password': 'your_password',
    'host': 'localhost',
    'database': 'your_database',
    'autocommit': False  # 关闭自动提交
}

def execute_sql(conn, sql, params=None):
    """执行 SQL 语句"""
    cursor = conn.cursor()
    try:
        cursor.execute(sql, params)
        if cursor.description:
            return cursor.fetchall()
        else:
            return None
    except mysql.connector.Error as err:
        print(f"Error: {err}")
        conn.rollback()
        return None
    finally:
        cursor.close()

def simulate_mvcc():
    """模拟 MVCC 行为"""
    try:
        # 创建两个连接,模拟两个事务
        conn1 = mysql.connector.connect(**config)
        conn2 = mysql.connector.connect(**config)

        # 事务 1:读取数据
        print("Transaction 1: Reading initial data...")
        result1 = execute_sql(conn1, "SELECT id, name FROM users WHERE id = 1")
        print(f"Transaction 1: Initial data: {result1}")

        # 事务 2:修改数据并提交
        print("Transaction 2: Updating data...")
        execute_sql(conn2, "UPDATE users SET name = %s WHERE id = 1", ('Bob',))
        conn2.commit()
        print("Transaction 2: Data updated and committed.")

        # 事务 1:再次读取数据
        print("Transaction 1: Reading data again...")
        result2 = execute_sql(conn1, "SELECT id, name FROM users WHERE id = 1")
        print(f"Transaction 1: Data after update: {result2}")

        # 事务 1:提交事务
        conn1.commit()
        print("Transaction 1: Committed.")

    except mysql.connector.Error as err:
        print(f"Error: {err}")
    finally:
        if conn1:
            conn1.close()
        if conn2:
            conn2.close()

if __name__ == "__main__":
    simulate_mvcc()

注意:

  • 请将 your_user, your_password, localhost, your_database 替换为你的实际数据库配置。
  • 确保 users 表存在,并包含 idname 列。

这个示例模拟了 RR 隔离级别下的 MVCC 行为。事务 1 在第一次读取数据后,会创建一个 Read View。即使事务 2 修改并提交了数据,事务 1 再次读取数据时,仍然会看到之前版本的数据,直到事务 1 提交。

总结

Read View 是 InnoDB MVCC 实现的核心组成部分,它定义了事务能够看到的数据版本。Read View 的创建时机和可见性判断规则与事务的隔离级别密切相关。理解 Read View 的工作原理对于理解 InnoDB 的并发控制和事务隔离级别至关重要。

Read View 的工作原理概括

Read View 通过记录活跃事务的信息,帮助 InnoDB 确定事务可以访问哪些数据版本。Read View 的创建时机和可见性判断规则是实现不同隔离级别的关键。 理解 Read View 是理解 InnoDB 并发控制的基础。

发表回复

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