MySQL的`Innodb`的`MVCC`:其`Read View`的创建与销毁

MySQL InnoDB MVCC:Read View 的创建与销毁

大家好,今天我们来深入探讨 MySQL InnoDB 存储引擎中多版本并发控制 (MVCC) 的核心机制之一:Read View 的创建与销毁。理解 Read View 如何工作,对于我们深入理解 InnoDB 的事务隔离级别和数据一致性至关重要。

什么是 MVCC?

MVCC 是一种并发控制方法,允许数据库在同一时间点维持多个版本的数据。这意味着在读取数据时,事务不会被阻塞,因为它可以读取一个历史版本的数据,而不是必须等待其他事务完成对数据的修改。

InnoDB 通过在每一行数据中存储版本信息来实现 MVCC。这些版本信息包括:

  • DB_TRX_ID: 创建或修改该行的事务 ID。
  • DB_ROLLBACK_PTR: 指向回滚段 (undo log) 的指针,用于恢复到之前的版本。

Read View 的作用

Read View 是 MVCC 实现的关键组件,它定义了事务可以读取哪些版本的数据。每个事务在启动时都会创建一个 Read View,Read View 包含了一系列信息,用于判断哪些版本的数据对该事务可见。

Read View 的核心组成部分:

  • trx_id: 创建该 Read View 的事务 ID。
  • m_ids: 当前活跃的事务 ID 列表。 存储了创建ReadView时,当前未提交的(active)事务ID的列表。
  • min_trx_id: m_ids 列表中最小的事务 ID。
  • max_trx_id: 下一个要分配的事务 ID,并非m_ids中的最大事务 ID。

Read View 的创建

Read View 的创建时机取决于事务的隔离级别:

  • READ COMMITTED (RC): 每次读取数据时都会创建一个新的 Read View。
  • REPEATABLE READ (RR): 事务第一次读取数据时创建一个 Read View,并在整个事务期间保持不变。

让我们通过一些伪代码来说明 Read View 的创建过程。

READ COMMITTED (RC) 隔离级别下的 Read View 创建:

ReadView* create_read_view_rc(trx_id_t trx_id) {
    ReadView* read_view = new ReadView();
    read_view->trx_id = trx_id;
    read_view->m_ids = get_active_transaction_ids(); // 获取当前活跃的事务 ID 列表
    read_view->min_trx_id = get_min_transaction_id(read_view->m_ids);
    read_view->max_trx_id = get_next_transaction_id();

    return read_view;
}

// 辅助函数,获取当前活跃的事务 ID 列表
std::vector<trx_id_t> get_active_transaction_ids() {
    std::vector<trx_id_t> active_ids;
    // 遍历事务系统,获取所有未提交的事务 ID
    // ...  (实际实现依赖于 InnoDB 的内部数据结构)
    return active_ids;
}

// 辅助函数,获取事务ID列表中最小的事务ID
trx_id_t get_min_transaction_id(const std::vector<trx_id_t>& ids) {
  if (ids.empty()) {
    return get_next_transaction_id(); // 如果没有活跃事务,返回下一个事务ID作为最小值,确保所有历史版本可见
  }
  trx_id_t min_id = ids[0];
  for (size_t i = 1; i < ids.size(); ++i) {
    if (ids[i] < min_id) {
      min_id = ids[i];
    }
  }
  return min_id;
}

// 辅助函数,获取下一个要分配的事务 ID
trx_id_t get_next_transaction_id() {
    // 从事务系统中获取下一个事务 ID
    // ... (实际实现依赖于 InnoDB 的内部数据结构)
    return next_transaction_id;
}

REPEATABLE READ (RR) 隔离级别下的 Read View 创建:

ReadView* create_read_view_rr(trx_id_t trx_id, ReadView* existing_read_view) {
    if (existing_read_view != nullptr) {
        // 如果已经存在 Read View,则直接返回
        return existing_read_view;
    }

    ReadView* read_view = new ReadView();
    read_view->trx_id = trx_id;
    read_view->m_ids = get_active_transaction_ids(); // 获取当前活跃的事务 ID 列表
    read_view->min_trx_id = get_min_transaction_id(read_view->m_ids);
    read_view->max_trx_id = get_next_transaction_id();

    return read_view;
}

REPEATABLE READ 隔离级别下,如果事务已经创建了一个 Read View,后续的读取操作将重用该 Read View,确保事务在整个过程中看到一致的数据版本。

Read View 的可见性判断

创建 Read View 之后,InnoDB 如何使用它来判断一个数据版本是否对当前事务可见? 这涉及到对 DB_TRX_ID 的判断。

判断规则如下:

  1. DB_TRX_ID < min_trx_id: 表示该版本是在创建 Read View 之前已经提交的事务创建的,对当前事务可见。
  2. DB_TRX_ID >= max_trx_id: 表示该版本是在创建 Read View 之后启动的事务创建的,对当前事务不可见。
  3. DB_TRX_ID == trx_id: 表示该版本是当前事务创建的,对当前事务可见。
  4. min_trx_id <= DB_TRX_ID < max_trx_id: 需要进一步判断 DB_TRX_ID 是否在 m_ids 列表中。
    • 如果在 m_ids 列表中,表示该版本是在创建 Read View 时仍未提交的事务创建的,对当前事务不可见。
    • 如果不在 m_ids 列表中,表示该版本是在创建 Read View 之前已经启动但之后提交的事务创建的,对当前事务可见。

用伪代码表示如下:

bool is_visible(trx_id_t row_trx_id, ReadView* read_view) {
    if (row_trx_id < read_view->min_trx_id) {
        return true; // 情况 1:可见
    }

    if (row_trx_id >= read_view->max_trx_id) {
        return false; // 情况 2:不可见
    }

    if (row_trx_id == read_view->trx_id) {
        return true; // 情况 3:可见
    }

    // 检查 row_trx_id 是否在 m_ids 列表中
    for (trx_id_t id : read_view->m_ids) {
        if (id == row_trx_id) {
            return false; // 情况 4:在列表中,不可见
        }
    }

    return true; // 情况 4:不在列表中,可见
}

示例:

假设我们有以下事务:

  • 事务 A (ID: 10) 已经提交。
  • 事务 B (ID: 12) 正在运行。
  • 事务 C (ID: 15) 正在运行。
  • 事务 D (ID: 18) 要启动一个事务,其事务ID将被分配为18。

现在,事务 D 启动并创建了一个 Read View:

  • trx_id = 18
  • m_ids = {12, 15}
  • min_trx_id = 12
  • max_trx_id = 19 (假设下一个事务 ID 是 19)

现在,我们来分析几个数据版本的可见性:

  • 版本 1:DB_TRX_ID = 10

    • 10 < 12 (小于 min_trx_id),因此该版本对事务 D 可见。
  • 版本 2:DB_TRX_ID = 12

    • 12 >= 1212 < 19 (在 min_trx_idmax_trx_id 之间)
    • 12m_ids 列表中,因此该版本对事务 D 不可见。
  • 版本 3:DB_TRX_ID = 15

    • 15 >= 1215 < 19 (在 min_trx_idmax_trx_id 之间)
    • 15m_ids 列表中,因此该版本对事务 D 不可见。
  • 版本 4:DB_TRX_ID = 18

    • 18 == 18 (等于 trx_id),因此该版本对事务 D 可见。 这通常是事务D 自己修改的数据。
  • 版本 5:DB_TRX_ID = 20

    • 20 >= 19 (大于等于 max_trx_id),因此该版本对事务 D 不可见。

Read View 的销毁

Read View 的销毁时机同样取决于事务的隔离级别:

  • READ COMMITTED (RC): 每次读取操作完成后,Read View 都会被销毁。因为每次读取都会创建一个新的 Read View。
  • REPEATABLE READ (RR): 事务提交或回滚后,Read View 才会被销毁。 因为 RR 隔离级别在事务期间使用同一个 ReadView。
void destroy_read_view(ReadView* read_view) {
    if (read_view != nullptr) {
        delete read_view;
    }
}

// 在 RC 隔离级别下:
void read_committed_transaction() {
  // ...
  ReadView* read_view = create_read_view_rc(current_transaction_id);
  // 使用 read_view 读取数据
  // ...
  destroy_read_view(read_view); // 读取完成后立即销毁
  // ...
}

// 在 RR 隔离级别下:
void repeatable_read_transaction() {
  ReadView* read_view = nullptr;

  try {
    // ...
    read_view = create_read_view_rr(current_transaction_id, read_view); // 仅在第一次读取时创建

    // 多次使用 read_view 读取数据
    // ...

    // 事务提交
    commit_transaction();
  } catch (...) {
    // 事务回滚
    rollback_transaction();
  }

  destroy_read_view(read_view); // 事务结束后销毁
}

总结:

Read View 是 InnoDB MVCC 的核心,它通过维护活跃事务 ID 列表,并结合数据行的 DB_TRX_ID,实现了对数据版本的可见性判断。 Read View 的创建和销毁时机取决于事务的隔离级别,READ COMMITTED 级别每次读取都创建和销毁,而 REPEATABLE READ 级别在事务期间重用同一个 Read View。 理解 Read View 的工作原理,有助于我们更好地理解 InnoDB 的并发控制和事务隔离机制。

发表回复

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