乐趣区

关于前端:MySQL源代码阅读一致性读的实现

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 record
should 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 updatelock 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 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 语句完结时敞开。
  • 可见性判断:生成快照时已提交的事务可见。
退出移动版