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
的判断。
判断规则如下:
DB_TRX_ID
<min_trx_id
: 表示该版本是在创建 Read View 之前已经提交的事务创建的,对当前事务可见。DB_TRX_ID
>=max_trx_id
: 表示该版本是在创建 Read View 之后启动的事务创建的,对当前事务不可见。DB_TRX_ID
==trx_id
: 表示该版本是当前事务创建的,对当前事务可见。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
= 18m_ids
= {12, 15}min_trx_id
= 12max_trx_id
= 19 (假设下一个事务 ID 是 19)
现在,我们来分析几个数据版本的可见性:
-
版本 1:
DB_TRX_ID
= 1010 < 12
(小于min_trx_id
),因此该版本对事务 D 可见。
-
版本 2:
DB_TRX_ID
= 1212 >= 12
且12 < 19
(在min_trx_id
和max_trx_id
之间)12
在m_ids
列表中,因此该版本对事务 D 不可见。
-
版本 3:
DB_TRX_ID
= 1515 >= 12
且15 < 19
(在min_trx_id
和max_trx_id
之间)15
在m_ids
列表中,因此该版本对事务 D 不可见。
-
版本 4:
DB_TRX_ID
= 1818 == 18
(等于trx_id
),因此该版本对事务 D 可见。 这通常是事务D 自己修改的数据。
-
版本 5:
DB_TRX_ID
= 2020 >= 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 的并发控制和事务隔离机制。