时间:2021-07-01 10:21:17 帮助过:17人阅读
在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在创建后会一直到事务结束时才被关闭。
上面已经对Read View进行了大致介绍,下面就来看一下InnoDB是如何判断记录是否对当前事务可见的吧。这里的入口是storage/innobase/row/row0sel.cc
的row_search_mvcc
方法。
假设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));
}
理一下这里判断的依据
如果这里不满足的话,会走到row_sel_build_prev_vers_for_mysql->row_vers_build_for_consistent_read
的调用,根据回滚段中的信息不断构建前一个版本信息直至当前事务可见。
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
方法中去聚簇索引中取可见版本数据。
本文通过InnoDB源码,介绍了Read View的基本数据结构和概念以及InnoDB中是如何通过创建的Read View来判断可见性。实际上Read View就是一个活跃事务的快照,并且RC和RR隔离级别都复用了同样结构的Read View来判断可见性,不同的是Read View的生命周期根据相应的隔离级别而有所不同。
MySQL官方手册
数据库内核月报
初探InnoDB MVCC源码实现
标签:types.h get lin binary active for was recovery sel