Innodb是一个反对MVCC(即多版本并发管制)的存储引擎,一致性读性能基于MVCC。本文基于MySQL 5.7的源代码探讨一致性读的原理,包含快照的创立、判断是否可见、快照的敞开等。
前提
- 本文基于mysql-5.7.29,为不便浏览,文中代码大量删减。
- 须要理解“快照读”:生成快照时未提交事务的更改不可见,已提交事务的更改可见。
事务构造体
事务的构造体定义位于storage/innobase/include/trx0trx.h:898
。与MVCC相干的局部如下:
struct trx_t { /* 事务ID */ trx_id_t id; /*!< transaction id */ /* 一致性读的快照 */ ReadView* read_view; /*!< consistent read view used in the transaction, or NULL if not yet set */ // 省略一大堆属性... // ...}
其中事务的ID是一个64位的非负整数,须要留神的是,只有读写事务会调配事务ID,只读事务是不会调配ID的。
每个事务领有各自的ReadView,以下简称快照。
ReadView源代码
在ReadView
中定义了一个m_ids
,它保留了处于沉闷状态的事务ID列表,用于判断其它事务的批改对以后事务是否可见。
class ReadView { /** * 创立这个快照的事务ID */ trx_id_t m_creator_trx_id; /** * 生成这个快照时处于沉闷状态的事务ID的列表, * 是个曾经排好序的列表 */ ids_t m_ids; /** * 高水位线:id大于等于 m_low_limit_id 的事务都不可见。 * 在生成快照时,它被赋值为“下一个待调配的事务ID”(会大于所有已调配的事务ID)。 */ trx_id_t m_low_limit_id; /** * 低水位线:id小于m_up_limit_id的事务都不可见。 * 它是沉闷事务ID列表的最小值,在生成快照时,小于m_up_limit_id的事务都曾经提交(或者回滚)。 */ trx_id_t m_up_limit_id; // 判断事务是否可见的办法 bool changes_visible(){} // 敞开快照的办法 void close(){} // ...}
判断更改是否可见
在ReadView
中, 有一个changes_visible()
办法,用于判断某个事务的更改对以后事务是否可见:
/* 判断某个事务的批改对以后事务是否可见 */bool changes_visible(){ /** * 可见的状况: * 1. 小于低水位线,即创立快照时,该事务曾经提交(或回滚) * 2. 事务ID是以后事务。 */ if (id < m_up_limit_id || id == m_creator_trx_id) { return(true); } if (id >= m_low_limit_id) { /* 高于水位线不可见,即创立快照时,该事务还没有提交 */ return(false); } else if (m_ids.empty()) { /* 创立快照时,没有其它沉闷的读写事务时,可见 */ return(true); } /** * 执行到这一步,阐明事务ID在低水位和高水位之间,即 id ∈ [m_up_limit_id, m_low_limit_id) * 须要判断是否属于在沉闷事务列表m_ids中, * 如果在,阐明创立快照时,该事务处于沉闷状态(未提交),批改对以后事务不可见。 */ // 获取沉闷事务ID列表,并应用二分查找判断事务ID是否在 m_ids中 const ids_t::value_type* p = m_ids.data(); return(!std::binary_search(p, p + m_ids.size(), id));}
由此可见,以后事务判断一个事务的批改是否可见,次要依附沉闷事务列表m_ids来判断。
对于具体的某一行记录,如何判断以后事务是否可见呢?
判断主键索引是否可见
对于主键索引中的每一个数据行,除了用户定义的字段,还有额定的零碎字段,包含:
- Transaction ID: 批改这行记录的事务ID。
- Roll Pointer: undo日志指针
有了记录行的事务ID,再调用changes_visible()
就能晓得这个记录对以后事务是否可见:
/**Checks that a record is seen in a consistent read.@return true if sees, or false if an earlier version of the recordshould be retrieved */bool lock_clust_rec_cons_read_sees(){ // 获取批改这个数据行的事务ID trx_id_t trx_id = row_get_rec_trx_id(rec, index, offsets); // 调用 changes_visible() 判断是否可见 return(view->changes_visible(trx_id, index->table->name));}
如果不可见呢?须要去undo日志中找之前的版本:
if (!lock_clust_rec_cons_read_sees(clust_rec, index, offsets, node->read_view)) { // 判断数据行不可见时,去找之前的版本 err = row_sel_build_prev_vers( node->read_view, index, clust_rec, &offsets, &heap, &plan->old_vers_heap, &old_vers, mtr); // ......}
判断辅助索引是否可见
假如通过一个辅助索引查问id的SQL:
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
。
/** * 判断辅助索引中的记录是否可见。 * 返回true时,可见。返回false时,不确定,须要去主键索引中查问。 */ bool lock_sec_rec_cons_read_sees(){ // 批改这个页的最大事务id trx_id_t max_trx_id = page_get_max_trx_id(page_align(rec)); // 判断是否可见,条件是 max_trx_id < m_up_limit_id return(view->sees(max_trx_id));}
具体来说:
- 当
max_trx_id
小于低水位线时,可见,因为以后事务创立快照时批改这个索引页的事务曾经提交。 - 当条件不成立时,无奈确定是否可见。此时须要到主键索引中查找,再依据后面的
changes_visible()
来判断。
代码如下:
/** * 无奈确定辅助索引是否可见时, * 先执行ICP(索引下推)判断索引是否匹配,如果匹配再去查主键索引。 * 如果条件不匹配,就解决下一个辅助索引记录。 */ if (!srv_read_only_mode && !lock_sec_rec_cons_read_sees(rec, index, trx->read_view)) { switch (row_search_idx_cond_check( buf, prebuilt, rec, offsets)) { case ICP_NO_MATCH: // ICP不匹配,不必再去看主键索引。间接解决下一条记录 goto next_rec; case ICP_OUT_OF_RANGE: err = DB_RECORD_NOT_FOUND; goto idx_cond_failed; case ICP_MATCH: // ICP满足条件时,查主键索引,判断是否可见 goto requires_clust_rec; } ut_error;}
获取沉闷事务列表
在创立快照时,获取沉闷的读写事务列表:
/** * 生成读写事务ID列表,计算高下水位线等。 * 在创立快照 MVCC::view_open() 时调用 */void ReadView::prepare(trx_id_t id){ // 创立快照的ID为以后事务ID m_creator_trx_id = id; // 高水位线是“下一个待调配事务ID” m_low_limit_no = m_low_limit_id = trx_sys->max_trx_id; /** * 零碎事务构造体(trx_sys)中会记录沉闷的事务ID列表(trx_sys->rw_trx_ids), * 如果有沉闷的读写事务,就从trx_sys复制读写事务ID列表到m_ids中 */ if (!trx_sys->rw_trx_ids.empty()) { copy_trx_ids(trx_sys->rw_trx_ids); } else { // 创立快照时没有沉闷的读写事务 m_ids.clear(); } // ...}
下面的代码获取到了m_ids
和高水位线,低水位线在ReadView::complete()
中计算:
void ReadView::complete(){ /* 低水位线是沉闷事务列表的最小值,即第1个沉闷事务ID */ m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id; // ...}
快照的生成
快照的生成定义在ReadView* trx_assign_read_view()
办法中:
ReadView* trx_assign_read_view(/*=================*/ trx_t* trx) /*!< in/out: active transaction */{ if (srv_read_only_mode) { // 只读模式不必生成快照 return(NULL); } else if (!MVCC::is_view_active(trx->read_view)) { // 如果曾经生成快照,会间接返回 trx_sys->mvcc->view_open(trx->read_view, trx); // 执行生成快照 } return(trx->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
语句在开始事务的时候创立一致性视图(仅在可反复读时无效),代码如下。
static int innobase_start_trx_and_assign_read_view(){ if (trx->isolation_level == TRX_ISO_REPEATABLE_READ) { trx_assign_read_view(trx); } else { // 隔离级别不是可反复读时,会输入一条warning push_warning_printf(thd, Sql_condition::SL_WARNING, HA_ERR_UNSUPPORTED, "InnoDB: WITH CONSISTENT SNAPSHOT" " was ignored because this phrase" " can only be used with" " REPEATABLE READ isolation level."); }}
快照的敞开
不同隔离级别,解决敞开快照有点不同。
在可反复读隔离级别下,快照会在事务完结时敞开。整个事务的多个SQL应用同一个快照。
static 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语句完结时敞开。
/* If the MySQL lock count drops to zero we know that the current SQLstatement 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语句完结时敞开。
- 可见性判断:生成快照时已提交的事务可见。