Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%

# 聊聊 mysql 的 MVCC

发表于 2020-04-26 分类于 Mysql ， C ， 数据结构 ， 源码 ， Mysql 阅读次数： 阅读次数： Disqus：

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

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

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

    /* 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_id` 和 `m_up_limit_id` 之间，则要判断它是否在 `m_ids` 中，如果在，不可见，如果不在，表示已提交，可见
   具体的代码捞一下看看

       /** 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 Committed` 和 `Repeated Read` 也不一样，那前面说的 `read view` 都能支持吗，又是怎么支持呢，假如这个 `read view` 是在事务一开始就创建，那好像能支持的只是 RR 事务隔离级别，其实呢，这是通过创建 `read view`的时机，对于 RR 级别，就是在事务的第一个 `select` 语句是创建，对于 RC 级别，是在每个 `select` 语句执行前都是创建一次，那样就可以保证能读到所有已提交的数据