MVCC

MySQL版本: 8.0

上一篇InnoDB的事务分析-Undo Log我们分析了Undo Log的结构,在InnoDB的事务并发控制中采用的是MVCC的方法,即多版本控制。当一个事务修改表中数据的某一行时,将旧版本的数据插入Undo Log中,假如事务需要回滚操作时,Undo Log则被用于还原旧版本数据。MVCC的作用是让事务在并行发生时,在一定隔离级别前提下,可以保证在某个事务中能实现一致性读,InnoDB提供四种事务级别, READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, 和 SERIALIZABLE. 默认的事务级别是 REPEATABLE READ.

InnoDB事务相关的数据结构

trx_sys_t

trx_sys_t是整个事务的管理系统,包括MVCC的控制模块和数据库所有的活跃事务,以及回滚段(rollback segments)管理.

1
2
3
4
5
6
7
8
9
10
11
12
13
/** The transaction system central memory data structure. */
struct trx_sys_t {
/* ... */
MVCC *mvcc; /*!< Multi version concurrency control manager */
/* ... */
volatile trx_id_t max_trx_id; /* 下一个事务被分配的ID */

std::atomic<trx_id_t> min_active_id; /* 最小的活跃事务ID */

trx_id_t rw_max_trx_id; /* 最大的活跃事务ID */

Rsegs rsegs; /* 回滚段 */
}

MVCC

1
2
3
4
5
6
7
8
9
10
11
/** The MVCC read view manager */
class MVCC {
private:
typedef UT_LIST_BASE_NODE_T(ReadView) view_list_t; /* Read View链表 */

/* 空闲的Read View链表 */
view_list_t m_free;

/* 已经关闭的Read View链表 */
view_list_t m_views;
};

Read View

1
2
3
4
5
6
7
8
9
10
11
class ReadView {
/* ... */
private:
trx_id_t m_low_limit_id; /* 大于这个ID的事务均不可见 */

trx_id_t m_up_limit_id; /* 小于这个ID的事务均可见 */

trx_id_t m_creator_trx_id; /* 创建该Read View的事务ID */

trx_id_t m_low_limit_no;
}

InnoDB的事务流程

MySQL官方文档的事务例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
START TRANSACTION
[transaction_characteristic [, transaction_characteristic] ...]

transaction_characteristic: {
WITH CONSISTENT SNAPSHOT
| READ WRITE
| READ ONLY
}

BEGIN [WORK]
COMMIT [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
ROLLBACK [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
SET autocommit = {0 | 1}

事务的启动流程:

我们以WITH CONSISTENT SNAPSHOT为例,即隔离级别REPEATABLE READ:

入口:

1
2
3
4
case SQLCOM_BEGIN:
if (trans_begin(thd, lex->start_transaction_opt)) goto error;
my_ok(thd);
break;

trans_begin():

  • 检查用户连接thd是否存在活跃事务, 假如没有就分配一个trx_t并初始化

  • 启动事务trx,将trx的状态置为TRX_STATE_ACTIVE

  • 事务隔离级别为TRX_ISO_REPEATABLE_READ,即为trx分配Read View, 并初始化Read View中的几个值:

    1
    m_low_limit_no = m_low_limit_id = m_up_limit_id = trx_sys->max_trx_id;
  • Read View添加至MVCC中的m_view

事务内的查询处理

当我们执行查询语句例如select,函数执行流程:

1
index_read()->row_search_mvcc()

row_search_mvcc()中我们通过lock_clust_rec_cons_read_sees()判断我们读到的Record是否满足可见性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool lock_clust_rec_cons_read_sees(
const rec_t *rec, /*!< in: user record which should be read or
passed over by a read cursor */
dict_index_t *index, /*!< in: clustered index */
const ulint *offsets, /*!< in: rec_get_offsets(rec, index) */
ReadView *view) /*!< in: consistent read view */
{
/* ... */

/* 获取该条Record的trx_id */
trx_id_t trx_id = row_get_rec_trx_id(rec, index, offsets);

/* 判断可见性 */
return (view->changes_visible(trx_id, index->table->name));
}

changes_visible()判断可见性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool changes_visible(trx_id_t id, const table_name_t &name) const
MY_ATTRIBUTE((warn_unused_result)) {
ut_ad(id > 0);

/* 假如trx_id小于Read view限制的最小活跃事务ID m_up_limit_id 或者等于正在创建的事务ID m_creator_trx_id
* 即均满足可见
*/
if (id < m_up_limit_id || id == m_creator_trx_id) {
return (true);
}

check_trx_id_sanity(id, name);

/* 假如trx_id大于最大活跃的事务ID m_low_limit_id, 即不可见*/
if (id >= m_low_limit_id) {
return (false);

} else if (m_ids.empty()) {
return (true);
}

/* ... */
}

假如我们当前查找的Record不满足可见性,我们需要通过Undo Log查找该Record多版本中符合可见的数据段:

row_sel_build_prev_vers_for_mysq()

  • 获取Record的回滚段指针:

    1
    roll_ptr = row_get_rec_roll_ptr(rec, index, offsets);
  • 获取Record的事务ID:

    1
    rec_trx_id = row_get_rec_trx_id(rec, index, offsets);
  • 解析Record的回滚段指针内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /** Decodes a roll pointer. */
    inline void trx_undo_decode_roll_ptr(
    roll_ptr_t roll_ptr, /*!< in: roll pointer */
    ibool *is_insert, /*!< out: TRUE if insert undo log */
    ulint *rseg_id, /*!< out: rollback segment id */
    page_no_t *page_no, /*!< out: page number */
    ulint *offset) /*!< out: offset of the undo
    entry within page */
    {
    ut_ad(roll_ptr < (1ULL << 56));
    *offset = (ulint)roll_ptr & 0xFFFF;
    roll_ptr >>= 16;
    *page_no = (ulint)roll_ptr & 0xFFFFFFFF;
    roll_ptr >>= 32;
    *rseg_id = (ulint)roll_ptr & 0x7F;
    roll_ptr >>= 7;
    *is_insert = (ibool)roll_ptr; /* TRUE==1 */
    }
  • 通过rseg_id回滚段ID检索Undo Tablespace的ID.

  • 获取Undo Log记录的Record数据.

我们将上面提及的数据结构trx_sys_t, MVCCRead View结合来看: trx_sys_t管理整个MySQL数据库的事务元信息,当有新事务启动时,为每一个事务分配trx_t,并且在REPEATABLE READ隔离级别下,只在一开始分配一个Read View,记录该事务内所有查询的Record的可见性(通过m_low_limit_idm_up_limit_id), 所以事务内的每一次查询只需要与这个唯一的Read View比较即可.

总结

结合上一篇InnoDB的事务分析-Undo Log我们能大致的梳理MySQL的事务的启动流程和事务内的查询流程, 通过MVCC数据库能保证事务的隔离级别并且避免了开销更大的行级锁. 后面我们会继续分析InnoDB的事务的提交和回滚流程.