之前月报涉及相关知识的有:

MySQL · 引擎特性 · InnoDB undo log 漫游

MySQL · 引擎特性 · InnoDB 事务子系统介绍

Undo log可以用来做事务的回滚操作,保证事务的原子性。同时可以用来构建数据修改之前的版本,支持多版本读。

InnoDB表数据组织方式是主键聚簇索引。二级索引通过索引键值加主键值组合来唯一确定一条记录。聚簇索引和二级索引都包含了DELETED BIT标记位来标识记录是否被删除,真正的删除在事务commit之后且没有读会引用该版本数据的时候。在聚簇索引上还有一些额外信息会存储,6字节的DB_TRX_ID字段,表示最近一次插入或者更新该记录的事务ID。7字节的DB_ROLL_PTR字段,指向该记录的rollback segment的undo log记录。6字节的DB_ROW_ID,当有新数据插入的时候会自动递增。当表上没有用户主键的时候,InnoDB会自动产生聚集索引,包含DB_ROW_ID字段。

对于聚簇索引,更新是在原记录位置更新,通过记录指向undo log的隐藏列来重构早期版本的数据。但对于二级索引,是没有聚簇索引上的这些隐藏列的,因此无法在原记录位置更新。当二级索引更新的时候,需要将原记录标记为删除,再插入新的数据记录。当快照读通过二级索引读取数据发现deleted标识或者更新的时候,如果二级索引页上无法判断可见性,InnoDB会查看聚簇索引上的记录行,通过行上的DB_TRX_ID判断可见性,找到正确的可见版本数据。

当用mvcc读取的时候(row_search_mvcc),对于聚簇索引,当拿到一条记录后,会先通过函数lock_clust_rec_cons_read_sees判断可见性,如果不可见会再构建老版本数据row_vers_build_for_consistent_read。

对于二级索引,拿到记录会先调用lock_sec_rec_cons_read_sees判断page上记录的最近一次修改trx id是否小于m_up_limit_id,如果小于即该page上数据可见,否则即调用row_search_idx_cond_check检查可见性,对于ICP,索引条件下推的,可以先判断索引条件是否满足条件,这样避免不满足条件的行回表;对于满足条件的行则回表查看可见性。

  1. row_prebuilt_t *prebuilt, ulint match_mode,
  2. ulint direction) {
  3. ...
  4. /* This is a non-locking consistent read: if necessary, fetch
  5. a previous version of the record */
  6. if (trx->isolation_level == TRX_ISO_READ_UNCOMMITTED) {
  7. /* Do nothing: we let a non-locking SELECT read the
  8. latest version of the record */
  9. } else if (index == clust_index) {
  10. ...
  11. } else {
  12. /* We are looking into a non-clustered index,
  13. and to get the right version of the record we
  14. have to look also into the clustered index: this
  15. is necessary, because we can only get the undo
  16. information via the clustered index record. */
  17. ut_ad(!index->is_clustered());
  18. if (!srv_read_only_mode &&
  19. !lock_sec_rec_cons_read_sees(rec, index, trx->read_view)) {
  20. /* We should look at the clustered index.
  21. However, as this is a non-locking read,
  22. we can skip the clustered index lookup if
  23. the condition does not match the secondary
  24. index entry. */
  25. switch (row_search_idx_cond_check(buf, prebuilt, rec, offsets)) {
  26. case ICP_NO_MATCH:
  27. goto next_rec;
  28. case ICP_OUT_OF_RANGE:
  29. err = DB_RECORD_NOT_FOUND;
  30. goto idx_cond_failed;
  31. case ICP_MATCH:
  32. goto requires_clust_rec;
  33. }
  34. ...
  35. }
  36. bool lock_sec_rec_cons_read_sees(
  37. const rec_t *rec, /*!< in: user record which
  38. should be read or passed over
  39. by a read cursor */
  40. const dict_index_t *index, /*!< in: index */
  41. const ReadView *view) /*!< in: consistent read view */
  42. ...
  43. trx_id_t max_trx_id = page_get_max_trx_id(page_align(rec));
  44. ut_ad(max_trx_id > 0);
  45. return (view->sees(max_trx_id));
  46. }

在Undo log中会记录TRX_UNDO_TRX_ID事务ID和TRX_UNDO_TRX_NO事务Commit时的number值。其他的信息可以参考。

当事务为读写事务的时候,事务会获取trx_id。

  1. /** Allocates a new transaction id.
  2. @return new, allocated trx id */
  3. UNIV_INLINE
  4. trx_id_t trx_sys_get_new_trx_id() {
  5. ut_ad(trx_sys_mutex_own());
  6. /* VERY important: after the database is started, max_trx_id value is
  7. divisible by TRX_SYS_TRX_ID_WRITE_MARGIN, and the following if
  8. will evaluate to TRUE when this function is first time called,
  9. and the value for trx id will be written to disk-based header!
  10. Thus trx id values will not overlap when the database is
  11. repeatedly started! */
  12. if (!(trx_sys->max_trx_id % TRX_SYS_TRX_ID_WRITE_MARGIN)) {
  13. trx_sys_flush_max_trx_id();
  14. }
  15. return (trx_sys->max_trx_id++);
  16. }

当事务commit时会获取新的系统trx id作为trx_no。

ReadView主要结构

  • m_low_limit_id。 事务ID大于等于该值的数据修改不可见

  • m_up_limit_id. 事务ID小于该值的数据修改可见。

  • m_creator_trx_id。创建该ReadView的事务,该事务ID的数据修改可见。

  • m_ids。当快照创建时的活跃读写事务列表。

  • m_low_limit_no。事务number,上一节介绍Undo log时候,事务提交时候获取同时写入Undo log中的值。事务number小于该值的对该ReadView不可见。利用该信息可以Purge不需要的Undo。

  • m_closed。 标记该ReadView closed,用于优化减少trx_sys->mutex这把大锁的使用。

    可以看到在view_close的时候如果是在不持有trx_sys->mutex锁的情况下,会仅将ReadView标记为closed,并不会把ReadView从m_views的list中移除。

    1. void MVCC::view_close(ReadView *&view, bool own_mutex) {
    2. uintptr_t p = reinterpret_cast<uintptr_t>(view);
    3. /* Note: The assumption here is that AC-NL-RO transactions will
    4. call this function with own_mutex == false. */
    5. if (!own_mutex) {
    6. /* Sanitise the pointer first. */
    7. ReadView *ptr = reinterpret_cast<ReadView *>(p & ~1);
    8. /* Note this can be called for a read view that
    9. was already closed. */
    10. ptr->m_closed = true;
    11. /* Set the view as closed. */
    12. view = reinterpret_cast<ReadView *>(p | 0x1);
    13. } else {
    14. view = reinterpret_cast<ReadView *>(p & ~1);
    15. view->close();
    16. UT_LIST_ADD_LAST(m_free, view);
    17. ut_ad(validate());
    18. view = NULL;
    19. }
    20. }

    当再次调用view_open的时候,如果trx上的read view在产生之后没有新的读写事务发生就可以不用生成新的ReadView,避免加锁添加到m_views中的操作。

    1. void MVCC::view_open(ReadView *&view, trx_t *trx) {
    2. ...
    3. if (view != NULL) {
    4. if (trx_is_autocommit_non_locking(trx) && view->empty()) {
    5. view->m_closed = false;
    6. if (view->m_low_limit_id == trx_sys_get_max_trx_id()) {
    7. return;
    8. } else {
    9. view->m_closed = true;
    10. }
    11. }
    12. }
    13. ...
    14. }
    • m_view_list 用于MVCC链表中前后节点信息存储。

ReadView可见性判断:

  • 如果记录trx_id小于m_up_limit_id或者等于m_creator_trx_id,表明ReadView创建的时候该事务已经提交,记录可见。

  • 如果记录的trx_id大于等于m_low_limit_id,表明事务是在ReadView创建后开启的,其修改,插入的记录不可见。

Class MVCC封装了ReadView相关的访问。内部成员变量有 m_free存放释放的read view用来reuse避免重新构造。m_views存放active和closed状态的read view。该类提供的主要函数有

  • clone_oldest_view(ReadView *view) 考虑最老的ReadView,用于purge线程清理deleted数据和不需要的旧版本数据。

    1. trx_purge(
    2. {
    3. trx_sys->mvcc->clone_oldest_view(&purge_sys->view);
    4. }
  • set_view_creator_trx_id(ReadView *view, trx_id_t id); 设置read view的creator trx id。

  • size() 处于活跃状态的read view数目

  • view_open(ReadView *&view, trx_t *trx); 创建read view。 view属于trx.

  • view_close(ReadView *&view, bool own_mutex); close read view。当own_mutext为false的时候,设置view为closed不去从m_views中移除。

  • view_release(ReadView *&view); release非活跃事务

  • is_view_active(ReadView *view) read view是否活跃

对于RC隔离级别或者设置innodb_locks_unsafe_for_binlog的情况下,当发生表扫描的UPDATE语句,如果数据行上有锁,UPDATE会先查看最近一次提交的数据是否满足条件,利用undo构建最近一次提交的数据。当满足条件再去读最新修改的行,这一次再等锁加锁,避免锁的等待。

  1. row_search_mvcc()
  2. {
  3. case DB_LOCK_WAIT:
  4. /* Lock wait for R-tree should already
  5. be handled in sel_set_rtr_rec_lock() */
  6. ut_ad(!dict_index_is_spatial(index));
  7. /* Never unlock rows that were part of a conflict. */
  8. std::fill_n(prebuilt->new_rec_lock, row_prebuilt_t::LOCK_COUNT, false);
  9. if (UNIV_LIKELY(prebuilt->row_read_type !=
  10. ROW_READ_TRY_SEMI_CONSISTENT) ||
  11. unique_search || index != clust_index) {
  12. goto lock_wait_or_error;
  13. }
  14. /* The following call returns 'offsets' associated with 'old_vers' */
  15. row_sel_build_committed_vers_for_mysql(clust_index, prebuilt, rec,
  16. &offsets, &heap, &old_vers,
  17. need_vrow ? &vrow : NULL, &mtr);
  18. }

这里当查询为unique_search并没有走semi consistent read,即对于’update t set … where pk = xx’的语句不会走semi consistent read。这里原因是bug#52663,在部分代码实现约束仅table scan的执行才可以。

同时Semi consistent read由于采用了非冲突串行化的处理方式,因此只能用在RC隔离级别或者设置innodb_locks_unsafe_for_binlog的情况下使用。

InnoDB的多版本并不是直接存储多个版本的数据,而是所有更改操作利用行锁做并发控制,这样对某一行的更新操作是串行化的,然后用Undo log记录串行化的结果。当快照读的时候,利用Undo log重建需要读取版本的数据,从而实现读写并发。