mvcc原理

参考

  • MySQL多版本并发控制机制(MVCC)-源码浅析 其版本似乎较旧,笔者用的是mysql8.0.13,因此一些代码的实现不同,但原理一致。
  • 初探InnoDB MVCC源码实现
  • MySQL InnoDB MVCC深度分析
    1. update、insert、delete都会保存当前id。
    2. update非主键时
      • 老记录被复制到rollback segment形成undo log,DB_TRX_ID和DB_ROLL_PTR不动。
      • 新记录的DB_TRX_ID = 当前事务ID,DB_ROLL_PTR指向老记录形成的undo log。
      • 这样就能通过DB_ROLL_PTR找到这条记录的历史版本。如果对同一行记录执行连续的update操作,新记录与undo log会组成一个链表,遍历这个链表可以看到这条记录的变迁)
  • mvcc undo log理解

本文不是从头介绍,需要读者先看完其它参考文章,对各种专有名词有一个概念。

环境

mysql 8.0.13源码

视图的创建

...
            |-index_read
                           |-row_search_mvcc

index_read有一个分支会调用row_search_no_mvcc,但这个只在表是intrinsic时才调用。intrinsic表示mysql的一个内部用的表,我们不用管它。 我们看下row_search_mvcc里的一个分支:

// 这边只有select不加锁模式的时候才会创建一致性视图
else if (prebuilt->select_lock_type == LOCK_NONE) {     // 创建一致性视图
        trx_assign_read_view(trx);
        prebuilt->sql_stat_start = FALSE;
}

上面的注释就是select for update(in share model)不会走MVCC的原因。让我们进一步分析trx_assign_read_view函数:

trx_assign_read_view
 |-MVCC::view_open
    |-MVCC::get_view
    |-ReadView::prepare

当view为空时,会调用view = get_view();得到一个空闲ReadView,然后调用view->prepare(trx->id);对其初始化

void ReadView::prepare(trx_id_t id) {
  // ut_ad的作用相当于assert,说明现在一定获取了事务系统的锁
  ut_ad(mutex_own(&trx_sys->mutex));

  // m_creator_trx_id: 创建该ReadView的事务的id
  m_creator_trx_id = id;

  // trx_sys->max_trx_id: 还未分配的最小事务id
  m_low_limit_no = m_low_limit_id = m_up_limit_id = trx_sys->max_trx_id;

  // 保存当前的事务id快照
  // rw_trx_ids: 当前的事务,可能是ACTIVE或PREPARED状态
  if (!trx_sys->rw_trx_ids.empty()) {
    copy_trx_ids(trx_sys->rw_trx_ids); // 函数内,把rw_trx_ids拷贝一份到m_ids,但自己的事务id除外。还设置了m_up_limit_id为m_ids首元素,也就是当前最小id。
  } else {
    m_ids.clear();
  }

  // ut_ad的作用如上,现在一定m_up_limit_id <= m_low_limit_id
  // 此时,m_low_limit_id是还未分配的最小事务id,m_up_limit_id是当前存在的事务的最小id。
  // 比如这样的情况: [存在的事务id1(也就是m_up_limit_id), 存在的事务id2, (不包括m_creator_trx_id), ... ,最大的事务id], m_low_limit_id(还未被分配)
  // 其中,中括号以内就是m_ids,m_ids不包括m_creator_trx_id
  ut_ad(m_up_limit_id <= m_low_limit_id);


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

  // 走到这里,上限m_low_limit_id是trx_sys->max_trx_id,下限m_up_limit_id是m_ids首元素,
  m_closed = false;
}

判断事务可见性

ReadView::changes_visible:

/** Check whether the changes by id are visible.
@param[in]  id  transaction id to check against the view
@param[in]  name    table name
@return whether the view sees the modifications of id. */
bool changes_visible(trx_id_t id, const table_name_t &name) const
    MY_ATTRIBUTE((warn_unused_result)) {
  ut_ad(id > 0);
  // 若记录的id小于下限,或就是记录的id就是事务的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等于trx_sys->max_trx_id,也就是还未分配的最小id。
  if (id >= m_low_limit_id) {
    return (false);

  } else if (m_ids.empty()) {
    return (true);
  }
  
  // 其实p就是m_ids的首元素的指针,m_ids是一个vector。
  const ids_t::value_type *p = m_ids.data();

  // 运行到这里,我们可以肯定id小于上限,而上限是还未分配的最小id,所以id一定属于某个事务,只是这个事务可能存在,也可能已经提交。
  // 用二分查找,在m_ids里找这个id,m_ids里的id在创建时都没有提交,因此都是不可见的。如果找得到,说明不可见。
  return (!std::binary_search(p, p + m_ids.size(), id));
}
  • 这个函数适用于RC和RR,因为在RC中,每次select时都会生成一次m_ids,对于最新的m_ids,最新的m_ids里的id在被过滤出来时都没有提交,因此都是不可见的。
  • 而在RR中:
    1. 只在第一次select时会生成m_ids,此时没提交的事务,在之后就算提交了,也不能可见。
    2. 此时提交了的事务,要么id小于下限,要么在生成m_ids时被过滤掉了,但都一定小于上限。
    3. 而之后生成的那些事务,其id一定都大于等于上限,因为上限等于trx_sys->max_trx_id

无锁才获取视图

// row0sel.cc row_search_mvcc
else if (prebuilt->select_lock_type == LOCK_NONE) {
    /* This is a consistent read */
    /* Assign a read view for the query */

    if (!srv_read_only_mode) {
      trx_assign_read_view(trx);
    }

    prebuilt->sql_stat_start = FALSE;
  } else {
  ...

prebuilt->select_lock_type == LOCK_NONE明确讲了,只有无锁情况下才会获取视图。而updatedeleteselect for update都是要锁的,不会触发视图的获取。

// trx0trx.cc
/** Assigns a read view for a consistent read query. All the consistent reads
 within the same transaction will get the same read view, which is created
 when this function is first called for a new started transaction.
 @return consistent 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);
}

所以,RR是在第一次select时才创建视图。(原文中提到了trx->global_read_view,但在我的版本的代码里没有找到,但不影响理解逻辑。)

select->update->select后,视图不一致

见原文MySQL多版本并发控制机制(MVCC)-源码浅析

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券