聊聊 mysql 的 MVCC

很久以前,有位面试官问到,你知道 mysql 的事务隔离级别吗,“额 O__O …,不太清楚”,完了之后我就去网上找相关的文章,找到了这篇MySQL 四种事务隔离级的说明, 文章写得特别好,看了这个就懂了各个事务隔离级别都是啥,不过看了这个之后多思考一下的话还是会发现问题,这么神奇的事务隔离级别是怎么实现的呢

其中 innodb 的事务隔离用到了标题里说到的 mvcc,Multiversion concurrency control, 直译过来就是多版本并发控制,先不讲这个究竟是个啥,考虑下如果纯猜测,这个事务隔离级别应该会是怎么样实现呢,愚钝的我想了下,可以在事务开始的时候拷贝一个表,这个可以支持 RR 级别,RC 级别就不支持了,而且要是个非常大的表,想想就不可行

腆着脸说虽然这个不可行,但是思路是对的,具体实行起来需要做一系列(肥肠多)的改动,首先根据我的理解,其实这个拷贝一个表是变成拷贝一条记录,但是如果有多个事务,那就得拷贝多次,这个问题其实可以借助版本管理系统来解释,在用版本管理系统,git 之类的之前,很原始的可能是开发完一个功能后,就打个压缩包用时间等信息命名,然后如果后面要找回这个就直接用这个压缩包的就行了,后来有了 svn,git 中心式和分布式的版本管理系统,它的一个特点是粒度可以控制到文件和代码行级别,对应的我们的 mysql 事务是不是也可以从一开始预想的表级别细化到行的级别,可能之前很多人都了解过,数据库的一行记录除了我们用户自定义的字段,还有一些额外的字段,去源码data0type.h里捞一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Precise data types for system columns and the length of those columns;
NOTE: the values must run from 0 up in the order given! All codes must
be less than 256 */
#define DATA_ROW_ID 0 /* row id: a 48-bit integer */
#define DATA_ROW_ID_LEN 6 /* stored length for row id */

/** Transaction id: 6 bytes */
constexpr size_t DATA_TRX_ID = 1;

/** Transaction ID type size in bytes. */
constexpr size_t DATA_TRX_ID_LEN = 6;

/** Rollback data pointer: 7 bytes */
constexpr size_t DATA_ROLL_PTR = 2;

/** Rollback data pointer type size in bytes. */
constexpr size_t DATA_ROLL_PTR_LEN = 7;

一个是 DATA_ROW_ID,这个是在数据没指定主键的时候会生成一个隐藏的,如果用户有指定主键就是主键了

一个是 DATA_TRX_ID,这个表示这条记录的事务 ID

还有一个是 DATA_ROLL_PTR 指向回滚段的指针

指向的回滚段其实就是我们常说的 undo log,这里面的具体结构就是个链表,在 mvcc 里会使用到这个,还有就是这个 DATA_TRX_ID,每条记录都记录了这个事务 ID,表示的是这条记录的当前值是被哪个事务修改的,下面就扯回事务了,我们知道 Read Uncommitted, 其实用不到隔离,直接读取当前值就好了,到了 Read Committed 级别,我们要让事务读取到提交过的值,mysql 使用了一个叫 read view 的玩意,它里面有这些值是我们需要注意的,

m_low_limit_id, 这个是 read view 创建时最大的活跃事务 id

m_up_limit_id, 这个是 read view 创建时最小的活跃事务 id

m_ids, 这个是 read view 创建时所有的活跃事务 id 数组

m_creator_trx_id 这个是当前记录的创建事务 id

判断事务的可见性主要的逻辑是这样,

  1. 当记录的事务 id 小于最小活跃事务 id,说明是可见的,
  2. 如果记录的事务 id 等于当前事务 id,说明是自己的更改,可见
  3. 如果记录的事务 id 大于最大的活跃事务 id, 不可见
  4. 如果记录的事务 id 介于 m_low_limit_idm_up_limit_id 之间,则要判断它是否在 m_ids 中,如果在,不可见,如果不在,表示已提交,可见
    具体的代码捞一下看看
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /** 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);

    if (id < m_up_limit_id || id == m_creator_trx_id) {
    return (true);
    }

    check_trx_id_sanity(id, name);

    if (id >= m_low_limit_id) {
    return (false);

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

    const ids_t::value_type *p = m_ids.data();

    return (!std::binary_search(p, p + m_ids.size(), id));
    }
    剩下来一点是啥呢,就是 Read CommittedRepeated Read 也不一样,那前面说的 read view 都能支持吗,又是怎么支持呢,假如这个 read view 是在事务一开始就创建,那好像能支持的只是 RR 事务隔离级别,其实呢,这是通过创建 read view的时机,对于 RR 级别,就是在事务的第一个 select 语句是创建,对于 RC 级别,是在每个 select 语句执行前都是创建一次,那样就可以保证能读到所有已提交的数据