当我们执行普通的select查询时,InnoDB会使用MVCC机制来实现记录的无锁一致性读取(non-locking consistent read),也就是在一个事务中对同一条记录多次读取结果保持一致,尽管可能有其他事务在修改该记录。
而MVCC机制主要由两部分来实现,一部分是由undo log组成的版本链,另一部分就是ReadView,本文主要聊聊ReadView的底层实现机制。
关注我,公众号:IT周瑜,后面再聊聊版本链的实现机制。
ReadView有三个重要的属性:
- m_low_limit_id,创建ReadView时,事务系统分配给下一个事务的事务id,事务id在源码中用trx_id表示
- m_up_limit_id,创建ReadView时,事务系统中所有活跃事务(不包括只读事务)中最小的事务id
- m_ids,创建ReadView时,系统中所有活跃事务的事务id
这三个属性控制了ReadView对行记录不同版本的可见性,在遍历版本链时,会返回对当前ReadView可见的第一个版本。
- 如果某个版本的trx_id >= m_low_limit_id,则表示该版本是本ReadView创建之后才创建的版本,因此对当前ReadView,不可见。
- 如果某个版本的trx_id < m_low_limit_id,则要判断该版本在本ReadView创建时是否已经提交了
- 如果某个版本的trx_id < m_up_limit_id,表示已经提交了,可见
- 如果某个版本的trx_id >= m_up_limit_id,表示未提交:
- 如果在m_ids中,则表示未提交的活跃事务,不可见
- 如果不在m_ids中,则表示未提交的不活跃事务,可见(这种情况比较少见,源码对这种情况是可见)
总结一句话:ReadView只对在生成ReadView时已经提交的版本可见,没提交的事务或后续新开启的事务对应的记录版本都是不可见。
另外,ReadView还有一个属性是m_creator_trx_id,表示当前是哪个事务创建了该ReadView,如果一个版本的trx_id == m_creator_trx_id,那就表示该版本是本事务自己创建的,肯定可见。
创建ReadView源码
void
MVCC::view_open(ReadView*& view, trx_t* trx)
{
ut_ad(!srv_read_only_mode);
/** If no new RW transaction has been started since the last view
was created then reuse the the existing view. */
if (view != NULL) {
...
} else {
// 创建ReadView要加锁,因为需要把系统中当前被激活的所有事务的no复制到ReadView中
mutex_enter(&trx_sys->mutex);
// 会从空闲ReadView链表中获取一个空间的ReadView对象,没有空闲的就会构建一个空的ReadView对象
view = get_view();
}
if (view != NULL) {
// 初始化m_creator_trx_id、m_low_limit_id、m_ids
view->prepare(trx->id);
// 初始化m_up_limit_id,为m_ids中最小的事务id
view->complete();
// 把view添加到m_views链表中
UT_LIST_ADD_FIRST(m_views, view);
ut_ad(!view->is_closed());
ut_ad(validate());
}
trx_sys_mutex_exit();
}
void
ReadView::prepare(trx_id_t id)
{
ut_ad(mutex_own(&trx_sys->mutex));
// 哪个事务创建本ReadView
m_creator_trx_id = id;
// 创建ReadView时,事务系统下一个要分配的事务id是什么,大于等于该事务id的不可见
m_low_limit_id = trx_sys->max_trx_id;
// 把当前系统中的所有活跃的读写事务id复制给m_ids
if (!trx_sys->rw_trx_ids.empty()) {
copy_trx_ids(trx_sys->rw_trx_ids);
} else {
m_ids.clear();
}
}
void
ReadView::complete()
{
/* The first active transaction has the smallest id. */
// 把m_ids中最小的事务id赋值给m_up_limit_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;
}
判断ReadView可见性
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,则表示该记录是在ReadView创建之前就已经提交的,所以可见
// 如果记录的事务id等于m_creator_trx_id,则表示该记录和ReadView是同一个事务,所以可见
if (id < m_up_limit_id || id == m_creator_trx_id) {
return(true);
}
// 如果记录的事务id大于等于m_low_limit_id,则表示该记录大于等于ReadView中的所有事务id
// 也就表示该记录是创建ReadView之后的其他事务修改的,因此对当前ReadView不可见
if (id >= m_low_limit_id) {
return(false);
} else if (m_ids.empty()) {
// 如果m_ids是空的,表示创建ReadView时没有其他记录,并且id<m_low_limit_id,也是可见的
return(true);
}
const ids_t::value_type* p = m_ids.data();
// 在m_ids中进行查找,找到了就不可见,没找到就可见,m_ids表示活跃的事务,如果记录的事务id不属于ReadView中活跃的事务id,则可见
return(!std::binary_search(p, p + m_ids.size(), id));
}