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
。
-
如果
trx_id_data
等于trx_id
,则该版本可见。这意味着数据版本是由当前事务创建的,因此肯定可见。
-
如果
trx_id_data
小于min_trx_id
,则该版本可见。这意味着数据版本是由在创建 Read View 之前就已经提交的事务创建的,因此对所有事务都可见。
-
如果
trx_id_data
大于或等于max_trx_id
,则该版本不可见。这意味着数据版本是由在创建 Read View 之后才启动的事务创建的,因此对当前事务不可见。
-
如果
trx_id_data
在min_trx_id
和max_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 的事务创建。
-
事务 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’。
- Read View:
-
事务 B 修改该行数据,将值改为 ‘Y’,并提交。 此时,该行数据有两个版本,一个版本的值为 ‘X’,事务 ID 为 5,另一个版本的值为 ‘Y’,事务 ID 为 11。
-
事务 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’。
- Read View:
例子 2:REPEATABLE READ 隔离级别
假设有三个事务 A、B 和 C,它们的事务 ID 分别为 10、11 和 12。 初始时,某行数据的值为 ‘X’,由事务 ID 为 5 的事务创建。
-
事务 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’。
- Read View:
-
事务 B 修改该行数据,将值改为 ‘Y’,并提交。 此时,该行数据有两个版本,一个版本的值为 ‘X’,事务 ID 为 5,另一个版本的值为 ‘Y’,事务 ID 为 11。
-
事务 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:
通过这两个例子,我们可以看到,不同的隔离级别下,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 的并发控制机制至关重要。