MVCC 多版本并发控制

MVCC 多版本并发控制

多版本并发控制

多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读(Read Committed,简称RC)和可重复读(Repeatable Read,简称RR)这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,要求很低,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。

基本思想

当多个事务同时执行时,会导致并发一致性问题,可以通过加锁来解决这个问题,但是加锁操作会影响性能,因此需要其他的办法来尽量避免加锁操作。在实际场景中读操作往往多于写操作,因此引入读写锁来避免不必要的加锁操作。在读写锁中,读和读是没有互斥关系的,但读和写操作仍然是互斥的。MVCC理用了多版本的思想,写操作更新最新的版本快照,而读操作取读旧版本的快照,没有互斥关系,从而解决读写冲突的问题。

在 MVCC 中事务的修改操作(DELETE、INSERT、UPDATE)会为数据行新增一个版本快照。

脏读和不可重复读最根本的原因是事务读取到其它事务未提交的修改。在事务进行读取操作时,为了解决脏读和不可重复读问题,MVCC 规定只能读取已经提交的快照。当然一个事务可以读取自身未提交的快照,这不算是脏读。

Undo 日志

MVCC 的多版本指的是多个版本的快照,快照存储在 Undo 日志中,该日志通过回滚指针 ROLL_PTR 把一个数据行的所有快照连接起来。

Read View

MVCC维护了一个Read View结构,Read View是事务进行快照读操作的时候生产的读视图,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务(未提交的事务)的 ID 列表。下面展示了它包含的几个字段,以及初始化过程。

Read View中的部分字段

// 当前最小未分配的事务id
trx_id_t	m_low_limit_id;

// m_ids列表中事务id最小的id
trx_id_t	m_up_limit_id;

// 创建view的事务id
trx_id_t	m_creator_trx_id;

// 创建view时处于active状态的读写事务列表,即还未提交的那些事务构成的列表
ids_t		m_ids;

Read View初始化

void ReadView::prepare(trx_id_t id)
{
    // 宏定义内容,只有在debug模式才会执行一次,可以忽略类似的这部分代码
	ut_ad(mutex_own(&trx_sys->mutex));

	m_creator_trx_id = id;

    // trx_sys->max_trx_id是当前最小未分配的事务id。
    // InnoDB内部维护一个max_trx_id的全局变量,每次需要申请一个新的trx_id,就获得max_trx_id,然后max_trx_id自增1
	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_ids不为空,m_up_limit_id取活跃事务最小id,否则,m_up_limit_id取m_low_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;
}

Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 Undo 日志里面的某个版本的数据。Read View 的可见性判断代码如下:

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
    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));
}

这里的判断依据有三个:

  1. id == m_creator_trx_id是判断事务id是否是创建该view的事务id,如果是,则一定可见
  2. id < m_up_limit_id,即事务id是否小于m_ids中的最小事务id,如果小于,说明事务不在当前还未提交的事务列表中,且在之前已经提交,因此可见
  3. id >= m_low_limit_id,即事务id在该ReadView创建时还未开启(准确说是还没被分配到事务id),因此不可见

在数据行快照不可使用的情况下,需要沿着 Undo 日志的回滚指针 ROLL_PTR 找到下一个快照,再进行上面的判断。沿着 Undo日志确定了可见的快照之后,就可以对该寻找到的快照进行读操作了。

参考文章

  • MVCC多版本并发控制
  • 初探InnoDB MVCC源码实现
  • CS-Notes 数据库系统原理

你可能感兴趣的:(MVCC 多版本并发控制)