当前位置:Gxlcms > 数据库问题 > 初探InnoDB MVCC源码实现

初探InnoDB MVCC源码实现

时间:2021-07-01 10:21:17 帮助过:17人阅读

trx_id_t m_low_limit_id; // 事务id<m_up_limit_id的修改对于当前读可见 trx_id_t m_up_limit_id; // 创建view的事务id trx_id_t m_creator_trx_id; // 创建view时处于active状态的读写事务列表,这里的ids_t可以简单看作是一个vector ids_t m_ids;

在InnoDB的事务定义(参考trx0trx.h头文件)中包含了一个字段用来表示该事务的Read View。

ReadView*   read_view;

在InnoDB进行进行一致性读时,会判断当前事务的Read View是否存在,如果不存在则get一个新的Read View(InnoDB对于Read View有复用的机制,所以如果不存在可以复用的Read View对象才会去显示地new一个新的出来)。下面是trx_assign_read_view方法实现:

ReadView*
trx_assign_read_view(
/*=================*/
    trx_t*      trx)    /*!< in/out: active transaction */
{
    ut_ad(trx->state == TRX_STATE_ACTIVE);

    if (srv_read_only_mode) {

        ut_ad(trx->read_view == NULL);
        return(NULL);

    } else if (!MVCC::is_view_active(trx->read_view)) {
        trx_sys->mvcc->view_open(trx->read_view, trx);
    }

    return(trx->read_view);
}

下面再来看一下Read View是如何初始化的。

void
ReadView::prepare(trx_id_t id)
{
    ut_ad(mutex_own(&trx_sys->mutex));

    m_creator_trx_id = id;

    // trx_sys->max_trx_id是当前最小未分配的事务id。
    m_low_limit_no = m_low_limit_id = trx_sys->max_trx_id;

    // 将当前只读事务的id拷贝到view中的m_ids。
    if (!trx_sys->rw_trx_ids.empty()) {
        copy_trx_ids(trx_sys->rw_trx_ids);
    } else {
        m_ids.clear();
    }

    // trx_sys->serialisation_list是事务提交时会加入的一个按照trx->no排序的列表。
    // 这里取列表中第一个(如果有的话)为m_low_limit_no供purge线程作为是否清理undo的依据。
    if (UT_LIST_GET_LEN(trx_sys->serialisation_list) > 0) {
        const trx_t*    trx;

        trx = UT_LIST_GET_FIRST(trx_sys->serialisation_list);

        if (trx->no < m_low_limit_no) {
            m_low_limit_no = trx->no;
        }
    }
}

void
ReadView::complete()
{
    // m_up_limit_id取活跃事务最小id。
    m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id;

    ut_ad(m_up_limit_id <= m_low_limit_id);

    m_closed = false;
}

对于Read Committed的隔离级别,在一致性读语句结束后,会关闭掉Read View,而对于Repeatable Read的隔离级别,Read View在创建后会一直到事务结束时才被关闭。

3 Read View如何判断可见性

上面已经对Read View进行了大致介绍,下面就来看一下InnoDB是如何判断记录是否对当前事务可见的吧。这里的入口是storage/innobase/row/row0sel.ccrow_search_mvcc方法。

3.1 走聚簇索引的情况

假设sql查询走的是聚簇索引,则通过下面的lock_clust_rec_cons_read_sees方法来判断记录rec是否对当前事务可见。

bool
lock_clust_rec_cons_read_sees(
    const rec_t*    rec,    
    dict_index_t*   index,
    const ulint*    offsets,
    ReadView*   view)   
{
    ut_ad(dict_index_is_clust(index));
    ut_ad(page_rec_is_user_rec(rec));
    ut_ad(rec_offs_validate(rec, index, offsets));

        // 对于InnoDB处于只读模式或者表为临时表的情况永远都是可见的。
    if (srv_read_only_mode || dict_table_is_temporary(index->table)) {
        ut_ad(view == 0 || dict_table_is_temporary(index->table));
        return(true);
    }


    // 获取行记录上的事务id。
    trx_id_t    trx_id = row_get_rec_trx_id(rec, index, offsets);

    // 判断是否可见。
    return(view->changes_visible(trx_id, index->table->name));
}

下面再来看看ReadView::changes_visible方法的实现源码:

bool changes_visible(
    trx_id_t        id,
    const table_name_t& name) const
    MY_ATTRIBUTE((warn_unused_result))
{
    ut_ad(id > 0);

    // 如果行记录上的id<m_up_limit_id或者等于m_creator_trx_id则可见。
    if (id < m_up_limit_id || id == m_creator_trx_id) {

        return(true);
    }

    check_trx_id_sanity(id, name);

    // 如果行记录上的id>=m_low_limit_id,则不可见。
    if (id >= m_low_limit_id) {

        return(false);

    } else if (m_ids.empty()) {

        return(true);
    }

    const ids_t::value_type*    p = m_ids.data();

    // 二分判断是否在m_ids中,如果存在则不可见。
    return(!std::binary_search(p, p + m_ids.size(), id));
}

理一下这里判断的依据

  • 记录的事务id为m_creator_trx_id即当前事务的修改,一定可见。
  • 记录的事务id<m_up_limit_id,说明Read View在初始化的时候,修改此记录的事务已经提交了,因此可见。
  • 记录的事务id>=m_low_limit_id,说明Read View在初始化的时候,修改改记录的事务还没开启(准确说是还没被分配到事务id),因此不可见。

如果这里不满足的话,会走到row_sel_build_prev_vers_for_mysql->row_vers_build_for_consistent_read的调用,根据回滚段中的信息不断构建前一个版本信息直至当前事务可见。

3.2 走二级索引的情况

bool
lock_sec_rec_cons_read_sees(
    const rec_t*        rec,    
    const dict_index_t* index,
    const ReadView* view)
{
    ut_ad(page_rec_is_user_rec(rec));

    if (recv_recovery_is_on()) {
        return(false);
    } else if (dict_table_is_temporary(index->table)) {
        return(true);
    }
    // 取索引页上的PAGE_MAX_TRX_ID字段。
    trx_id_t    max_trx_id = page_get_max_trx_id(page_align(rec));

    ut_ad(max_trx_id > 0);

    return(view->sees(max_trx_id));
}

下面是ReadView:sees的实现,可以看到其实就是判断是否PAGE_MAX_TRX_ID小于ReadView初始化时的最小事务id,也就是判断修改页上记录的最大事务id是否在快照生成的时候已经提交了,简单粗暴的很。

bool sees(trx_id_t id) const
{
    return(id < m_up_limit_id);
}

因此这里lock_sec_rec_cons_read_sees方法如果返回true,那么是一定可见的,返回false的话未必不可见,但下一步就需要利用聚簇索引来获取可见版本的数据了。
在这之前InnoDB会先利用ICP(Index Push Down)根据索引信息来判断搜索条件是否满足,如果不满足那也没必要再去聚簇索引中取了;若ICP判断出符合条件,则会走到row_sel_get_clust_rec_for_mysql方法中去聚簇索引中取可见版本数据。

4. 总结

本文通过InnoDB源码,介绍了Read View的基本数据结构和概念以及InnoDB中是如何通过创建的Read View来判断可见性。实际上Read View就是一个活跃事务的快照,并且RC和RR隔离级别都复用了同样结构的Read View来判断可见性,不同的是Read View的生命周期根据相应的隔离级别而有所不同。

5. 参考

MySQL官方手册
数据库内核月报

初探InnoDB MVCC源码实现

标签:types.h   get   lin   binary   active   for   was   recovery   sel   

人气教程排行