详解Mysql的MVCC实现原理

目录

一.简介

二.适用场景

三.实现原理

        3.1 隐藏字段

        3.2 undo log日志

        3.3 Read View快照

        3.4 可见性逻辑

四.总结


一.简介

        MVCC(Multi-Version Concurrency Control):多版本并发控制机制

        这是一种InnoDB引擎独有的一种控制事务隔离级别的机制,通过巧妙的设计处理写-读冲突,避免加锁造成的性能损耗,提高了数据库的并发性能。

二.适用场景

        首先让我们先复习下数据库的四个隔离级别:

        读未提交(Read uncommitted)

        读已提交(Read committed):解决脏读问题

        可重复读(Repeatable read):解决脏读和不可重复读问题

        可串行化(Serializable):解决脏读,不可重复读和幻读问题

        在了解MVCC前,可能很多人(包括我)原本认为:

        mysql通过加上了写锁解决不可重复读的问题,在可重复读的隔离级别下,某个事务修改某一行的时候,其他事务无法对这一行数据进行修改和读取。

        这是错误的!

        事实上,四个隔离级别底层原理是这样实现的:

        读未提交(Read uncommitted)

        读已提交(Read committed):mvcc

        可重复读(Repeatable read):mvcc

        可串行化(Serializable):mvcc+间隙锁

        也就是说,MVCC机制解决了原本我们以为必须要加写锁才能解决的并发问题,大大提升了数据库在并发情况下的性能。

        不过需要注意的是,写与写依旧是冲突的,InnoDB引擎无论在什么级别下,在修改数据前会添加排他锁,确保其他事务只能对当前行数据进行读取而不能修改,完成数据修改后再释放排他锁。

        那么这么神奇的MVCC机制是如何实现的呢?

三.实现原理

        mvcc主要基于四个东西实现:DB_TRX_ID , DB_ROLL_PTR , undo log日志和Read View快照

        3.1 隐藏字段

         InnoDB引擎向数据库中的每一行添加了三个字段(使用者不可见)

        DB_TRX_ID:标识当前行内数据的唯一事务ID,每次修改或删除后对事务ID进行+1。

        DB_ROLL_PTR: 回滚指针,指向undo log中记录的上一个版本

        DB_ROW_ID: 众所周知,InnoDB引擎默认使用b+树作为存储的底层结构,而b+树是一种有序的数据结构。InnoDB引擎会默认将用户指定的主键设置为聚簇索引(b+树排序的根据),但如果没有设置主键,则将该字段设置为聚簇索引,与MVCC无关

        

        3.2 undo log日志

        undo log日志中会存储包含隐藏字段的历史版本

        例如,我们新建一个User表

CREATE TABLE User
(
id int PRIMARY KEY,
username VARCHAR(8)
)
insert into User VALUES (1,'大黄');

        那么这个表的结构就是这样的(因为DB_ROW_ID与MVCC无关就省略了)

        因为此时DB_TRX_ID仅为1,不存在历史版本信息,因此undo log是空的,让我们修改该行数据

update User set username = '小黑' where id = 1;

        修改数据后,DB_TRX_ID递增,DB_ROLL_PTR指向undo log中历史版本信息

        再次修改

update User set username = '旺财' where id = 1;

         可以了解到DB_ROLL_PTR以一种链表的形式连接所有历史版本

 

        3.3 Read View快照

        翻看源码,得知Read View的主要结构如下图所示(还有一些和视图相关的可能存在的字段,与MVCC核心实现相关不大,因此暂不概述)

解释下各个字段的意义:

        m_low_limit_id:创建视图时还未被分配的最小事务ID,即最大活跃事务ID+1(高水位线)

        m_up_limit_id:创建视图时不包含自身的活跃事务列表中的最小事务ID(低水位线)

        m_creator_trx_id:申请创建视图的事务ID

        m_ids:创建视图时不包含自身的处于活跃的事务的ID集合

        小小吐槽一下,这个low和up真的没有命名反吗?

而在不同隔离级别下,创建Read View快照的策略有所不同:

        读已提交:每次事务发起select查询语句时创建快照

        可重复读:只有当前事务第一次发起select查询语句时创建快照

        让我们举个例子:

当事务2第一次发起select查询时,只有事务1已经提交,而事务3,4已经开启,因此:

        m_low_limit_id(最大活跃事务ID+1)= 5

        m_up_limit_id(最小活跃事务ID,不包含自身)= 3

        m_creator_trx_id(发起创建快照的当前事务ID)= 2

        m_ids(当前活跃事务ID集合,不包含自身)= {3,4}

        

如果是读已提交级别,那么当事务2第二次发起select查询时,则会创建新的Read View快照,此时事务3已经提交:

        m_low_limit_id(最大活跃事务ID+1)= 5

        m_up_limit_id(最小活跃事务ID,不包含自身)= 4

        m_creator_trx_id(发起创建快照的当前事务ID)= 2

        m_ids(当前活跃事务ID集合,不包含自身)= {4}

        3.4 可见性逻辑

        MVCC实质上是综合以上结构,使用视图判断当前行数据是否可见来处理写读冲突,如果不可见,则会通过回滚指针回滚到历史版本再次进行可见性判断,直到找到第一个可见的历史版本返回,即为select查询到的数据,实现了读已提交可重复读的隔离级别。

        翻看源码可以得知,核心判断可见性逻辑是由该方法实现:

  /** 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. */
  [[nodiscard]] bool changes_visible(trx_id_t id,
                                     const table_name_t &name) const {
    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));
  }

让我们逐行解释:

入参

  • id:要检查的事务 ID。
  • name:当前操作涉及的表名,用于在检查事务 ID 时确保上下文正确。

逻辑说明

  1. ut_ad(id > 0):进行断言检查,确保传入的 id 参数大于 0。

  2. if (id < m_up_limit_id || id == m_creator_trx_id)

    如果要检查的事务 ID 小于最小活跃事务 ID 或等于当前事务 ID,则可见,返回 true
  3. check_trx_id_sanity(id, name):验证该事务 ID 是否与指定表 (name) 的状态一致。

  4. if (id >= m_low_limit_id)

    如果要检查的事务 ID 大于等于最小未被分配的事务 ID(当前最大事务 ID + 1),那么一定不可见,返回 false
  5. else if (m_ids.empty())

    在要检查的事务 ID 小于最小未被分配的事务 ID 的基础上,如果当前活跃事务集合为空,那么一定可见,返回 true
  6. const ids_t::value_type *p = m_ids.data();
    return (!std::binary_search(p, p + m_ids.size(), id))

    二分查找当前活跃事务集合中是否存在要检查的事务 ID。如果不存在,则可见,返回 true;如果存在,则不可见,返回 false

让我们再拿先前的例子开始讲解:

        

        如果事务隔离级别为可重复读,那么第二次查询时可见数据仍然为事务ID=2的数据,实现了可重复读

        如果为读已提交,则会重新创建一份视图快照:

        至于其他一些情况,我就不带着大家一起枚举或者进行数学逻辑上的验证了,感兴趣的小伙伴们可以自己尝试下。

         MVCC便是通过上述结构,将可见性的问题进行层层分离,只使用了一些简单的逻辑判断便解决了事务的隔离级别和读-写冲突问题,让人不得不佩服制作者的巧思。

        由此,我们就理解了MVCC是如何巧妙地处理了写-读冲突,实现了读已提交和可重复读的隔离级别。不过对于串行的隔离级别来说,我们就不得不加上间隙锁实现一个表锁的功能了。

四.总结

        InnoDB引擎通过MVCC机制,巧妙地解决了并发场景下的写-读冲突。了解了MVCC的核心原理,对于以后我们自己设计一些高并发下调优的架构也有所帮助。

本文就到此结束了,希望能对大家有所帮助,祝大家都能在秋招中收获想要的offer🌷🌷🌷

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值