Innodb是一个支持MVCC(即多版本并发控制)的存储引擎,一致性读功能基于MVCC。本文基于MySQL 5.7的源代码讨论一致性读的原理,包括快照的创建、判断是否可见、快照的关闭等。
前提
- 本文基于mysql-5.7.29,为方便阅读,文中代码大量删减。
- 需要了解“快照读”:生成快照时未提交事务的更改不可见,已提交事务的更改可见。
事务结构体
事务的结构体定义位于storage/innobase/include/trx0trx.h:898
。与MVCC相关的部分如下:
1 | struct trx_t { |
其中事务的ID是一个64位的非负整数,需要注意的是,只有读写事务会分配事务ID,只读事务是不会分配ID的。
每个事务拥有各自的ReadView,以下简称快照。
ReadView源代码
在ReadView
中定义了一个m_ids
,它保存了处于活跃状态的事务ID列表,用于判断其它事务的修改对当前事务是否可见。
1 | class ReadView { |
判断更改是否可见
在ReadView
中, 有一个changes_visible()
方法,用于判断某个事务的更改对当前事务是否可见:
1 | /* 判断某个事务的修改对当前事务是否可见 */ |
由此可见,当前事务判断一个事务的修改是否可见,主要依靠活跃事务列表m_ids来判断。
对于具体的某一行记录,如何判断当前事务是否可见呢?
判断主键索引是否可见
对于主键索引中的每一个数据行,除了用户定义的字段,还有额外的系统字段,包括:
- Transaction ID: 修改这行记录的事务ID。
- Roll Pointer: undo日志指针
有了记录行的事务ID,再调用changes_visible()
就能知道这个记录对当前事务是否可见:
1 | /** |
如果不可见呢?需要去undo日志中找之前的版本:
1 | if (!lock_clust_rec_cons_read_sees(clust_rec, index, offsets, |
判断辅助索引是否可见
假设通过一个辅助索引查询id的SQL:
1 | select id from t where idx=2; |
通过idx=2
定位到索引记录时,仅从辅助索引就可以获取到对应的id。那此时能否直接返回这个id呢?
显然不能。
与主键索引不同,辅助索引并没有保存修改这条记录的事务id,因此并不能判断idx=2
对应的记录是否可见。例如这条记录在当前事务创建快照之后,才被另外一个事务创建,那此时就是不可见的(隔离级别为可重复读时)。
对于辅助索引,每个数据页有一个字段:PAGE_MAX_TRX_ID
,保存了修改这个数据页的最大事务ID。
因此,判断辅助索引中的记录是否可见时,判断条件为:max_trx_id < m_up_limit_id
。
1 | /** |
具体来说:
- 当
max_trx_id
小于低水位线时,可见,因为当前事务创建快照时修改这个索引页的事务已经提交。 - 当条件不成立时,无法确定是否可见。此时需要到主键索引中查找,再根据前面的
changes_visible()
来判断。
代码如下:
1 | /** |
获取活跃事务列表
在创建快照时,获取活跃的读写事务列表:
1 | /** |
上面的代码获取到了m_ids
和高水位线,低水位线在ReadView::complete()
中计算:
1 | void ReadView::complete() |
快照的生成
快照的生成定义在ReadView* trx_assign_read_view()
方法中:
1 | ReadView* trx_assign_read_view( |
调用trx_assign_read_view()
方法主要有2个地方:
row_search_mvcc()
在执行无锁的select时会调用,即select不带for update
、lock in share mode
。innobase_start_trx_and_assign_read_view()
使用start transaction with consistent snapshot
语句在开始事务的时候创建一致性视图(仅在可重复读时有效),代码如下。
1 | static int innobase_start_trx_and_assign_read_view() |
快照的关闭
不同隔离级别,处理关闭快照有点不同。
在可重复读隔离级别下,快照会在事务结束时关闭。整个事务的多个SQL使用同一个快照。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29static void trx_commit_in_memory(){
if (trx_is_autocommit_non_locking(trx)) { // 自动提交的非锁定一致性读
if (trx->read_view != NULL) {
// 执行关闭快照
trx_sys->mvcc->view_close(trx->read_view, false);
}
} else {
if (trx->id > 0) {
/* trx->id 大于0,说明当前事务是读写事务。
* 需要从系统事务结构体(trx_sys->rw_trx_ids)的读写事务列表中移除当前事务ID,并关闭视图
*/
/* For consistent snapshot, we need to remove current
transaction from running transaction id list for mvcc
before doing commit and releasing locks. */
trx_erase_lists(trx, serialised);
}
// 只读事务
if (trx->read_only || trx->rsegs.m_redo.rseg == NULL) {
// 快照不为空时关闭快照
if (trx->read_view != NULL) {
trx_sys->mvcc->view_close(trx->read_view, false);
}
}
}在读已提交隔离级别下,快照会在SQL语句结束时关闭。
1
2
3
4
5
6
7
8
9
10
11
12
13
14/* If the MySQL lock count drops to zero we know that the current SQL
statement has ended */
if (trx->n_mysql_tables_in_use == 0) { // 当前SQL使用到的表格是数0,说明SQL执行结束了
// 隔离级别为读已提交时,执行 关闭快照
if (trx->isolation_level <= TRX_ISO_READ_COMMITTED
&& MVCC::is_view_active(trx->read_view)) {
trx_sys->mvcc->view_close(trx->read_view, true);
}
}
总结
- 执行无锁的select语句时会生成快照
- 在可重复读隔离级别下,快照会在事务结束时关闭。
- 在读已提交隔离级别下,快照会在SQL语句结束时关闭。
- 可见性判断:生成快照时已提交的事务可见。