暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Mysql之MVCC原理分析

Coding的哔哔叨叨 2020-12-25
292

最近在看mysql的mvcc的相关内容,发现网上好多文章写得都不尽人意,什么基于createVersion、deleteVersion来实现,好像也很对的样子,但实际很多是错误的,我也是翻阅了好多资料,才最终确定了MVCC的相关知识点,这儿拿来记录下。
写在最前,本文内容均是基于Mysql的InnoDB引擎来进行的,且一定注意MVCC是基于事务的,也就是说在MyISAM下是不支持的,所以一定要心中有底。
本文如无特别说明,都是基于InnoDB默认隔离级别,即RR(可重复读)下来进行的。


MVCC概念

Coding的哔哔叨叨

MVCC,全称Multi-Version Concurrency Control
,即多版本并发控制。MVCC是一种并发控制的方法,实现对数据库的并发访问,提高读操作的性能。只在写写之间相互阻塞,其他场景如读读、读写、写读都可以并行,大大提高mysql的并发度;是实现事务隔离性的关键。

注意MVCC只在 READ COMMITTED
REPEATABLE READ

两个隔离级别下工作。其他两个隔离级别不和MVCC不兼容, 因为READ UNCOMMITTED
总是读取最新的数据行, 而不是符合当前事务版本的数据行。而 SERIALIZABLE
则会对所有读取的行都加锁。



Mysql默认隔离级别是RR(可重复读),是通过“行锁+MVCC”来实现的,正常读时不加锁,写时加锁,MVCC的实现依赖于:三个隐藏字段Read ViewUndo log 来实现。

隐藏字段

Coding的哔哔叨叨

Innodb存储引擎,在每行数据的后面都有三个隐藏字段(其中两个与mvcc有关系):
  • DB_TRX_ID(6字节)
    DB_TRX_ID表示最近一次对本行记录行做修改(insert|update|delete)的事务ID。这儿注意,delete操作,innodb也认为是一个update操作,会更新一个删除为delete_bit,将行表示为deleted,并非真正的删除。

  • DB_ROLL_PTR(7字节)
    DB_ROLL_PTR表示回滚指针,指向当前记录行的undo log信息。

  • DB_ROW_ID(6字节)
    当表没有设置主键或唯一非空索引时,innodb便会使用这个row_id自动生成聚簇索引。如果表有主键或唯一索引,聚簇索引就不会包含这个字段,所以这个字段MVCC关系不大。

      注意,添加的隐藏字段并不是很多人认为的创建时间和删除时间,同时在MySQL中
    MVCC的实现也不是通过什么快照来实现的。之所以有这种说法可能是源自于《高性能
    MySQL》一书中对MySQL中的MVCC的错误结论,然后就人云亦云传开了(注意,我这
    里一直强调是MySQL中的MVCC的实现,是因为在不同的数据库中可能会有不同的实
    现)。所以说看源码和看官方文档才是最权威的解释。
    附上官网描述:
      InnoDB Muti-version:
      https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html

      Read View

      Coding的哔哔叨叨

      Read View即读视图,和快照、snapshot是一个意思。
      Read View主要是用来做可见性判断的,里边保存了“对本事务不可见的其他事务id列表”,我们结合Read View源码来进行解释。
        源码地址:
        https://github.com/facebook/mysql-5.6/blob/42a5444d52f264682c7805bf8117dd884095c476/storage/innobase/include/read0read.h#L125

        1. low_limit_id,目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。

        2.  up_limit_id活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id 为 low_limit_id。
           因为trx_ids中的活跃事务号是逆序的,所以最后一个为最小活跃事务ID。(up_limit_id 并不是已提交的最大事务ID+1,后面的会证明这是错误的)

        3. trx_ids:Read View创建时其他未提交的活跃事务ID列表。意思就是创建Read View时,将当前未提交事务ID记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。
          注意:Read View中trx_ids的活跃事务,不包括当前事务自己和已提交的事务(正在内存中)。

        4. creator_trx_id:当前创建事务的ID,是一个递增的编号。这个编号并不是数据行里的DB_ROW_ID。

        undo log

        Coding的哔哔叨叨

        Undo log中存储的是老版本数据,当一个事务需要读取记录行时,如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本。

        大多数对数据的变更操作包括 insert/update/delete,在InnoDB里,undo log分为如下两类:

        1. insert undo log : 事务对insert新记录时产生的undo log,只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。
        2. update undo log : 事务对记录进行delete和update操作时产生的undo log,不仅在事务回滚时需要,快照读也需要,所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除。
        3. 事务回滚后会将此事务所有的undo log都删除。
          purge
          线程:

          为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下旧记录的
          deleted_bit,并不真正将旧记录删除。
              为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true
          的记录。purge线程自己也维护了一个read view,如果某个记录的deleted_bit
          true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定
          是可以被安全清除的。


          修改数据行的具体流程

          Coding的哔哔叨叨

          聊完了上面的这些概念知识,下面我们模拟一次修改行的具体流程。

          假设有一行数据,最后一次修改的事务id是1:

          id
          name
          age
          db_row_id
          db_trx_id
          db_roll_ptr
          1
          Bob
          18
          -
          10x1234

          现在有一个事务A,对该行数据进行修改,将name改为“Jack”。

          1. 事务A将该行数据加行级排它锁。
          2. 然后将该行数据拷贝到undo log中,作为旧版本。
          3. 拷贝完成后,将该行数据的name改为“Jack”,修改事务id为2(事务A的id),并修改db_roll_ptr指针指向拷贝到undo log中的旧版本数据。
          4. 事务提交,释放排它锁,并将修改后的最新数据写入redo log中。(redo log的相关知识我会在后面也写一篇文章来专门去讲,所以这儿大家不必深究)。

          接着又有一个事务B来修改该行数据,修改age为35。

          1. 事务B将该行数据加行级排它锁。
          2. 然后将该行数据拷贝到undo log中,作为旧版本。
          3. 拷贝完成后,将该行数据的name改为“Jack”,修改事务id为3(事务B的id),并修改db_roll_ptr指针指向拷贝到undo log中的旧版本数据。
          4. 事务提交,释放排它锁,并将修改后的最新数据写入redo log中(这儿描述有误,是在事务提交前写入)。

          从上面的执行流程图中,我们可以清晰的看到,事务对同一行数据的修改,会使该数据的undo log形成一个链表,链首就是最新的旧记录,链尾就是最老的旧记录。


          可见性比较算法

          Coding的哔哔叨叨

          在InnoDB中,创建一个新的事务(本事务),在本事务中执行第一个select语句的时候,会创建一个快照(read view),在快照中,保存了当前mysql服务中,不该被本事务看到的其他活跃事务id列表(trx_ids)。当用户在本事务中要读取表中某行的数据时,InnoDB会用该行的DB_TRX_ID
          (最新修改本行数据的事务id)记为trx_id
          与此read view中的变量up_limit_id
          (活跃事务列表trx_ids中最老的事务id)、low_limit_id
          (生成此read view时,系统中的最大事务id+1,即还未分配的事务id)、trx_ids
          (其他活跃事务id,不包含本事务)进行比较,判断其是否满足可见性条件。
          具体的比较算法如下:
          1. trx_id < up_limit_id
            时,表示对此行数据最新一次修改的事务在本事务创建此read view前,就已经commit了,所以该行数据的值对本事务可见,直接跳到下面情况2

          2. trx_id >= low_limit_id时,表示本事务创建此read view后才修改的该行数据,该行数据对本事务不可见,直接跳到下面情况1.

          3. up_limit_id <= trx_id < low_limit_id时,表示本事务创建此read view的时候,系统有事务可能正在对该行数据进行修改还未提交事务,也有可能已经完成修改且已经提交事务,此时就需要用到trx_ids来进一步的判断。

            1. 若trx_id在trx_ids中找到了,说明本事务创建此read view前,id为trx_id的事务正在对该行数据进行修改,事务还未commit;也可能是在本事务创建此read view后,id为trx_id的事务对该行数据进行了修改(不论此时该trx_id事务有没有commit),该行数据对本事务不可见,直接跳到下面情况1

            2. 若trx_id在trx_ids中没找到,说明本事务创建此read view前,已经提交了事务trx_id,直接跳到下面情况2

          情况1:在该行数据的DB_Roll_PTR指针指向的undo log链中,取出最近的旧事务号DB_TRX_ID,将其值赋给trx_id,然后调回步骤1重新开始新一轮的判断。
          情况2:该行数据可见,将该行数据返回。
          比较算法源码如下图:
            https://github.com/facebook/mysql-5.6/blob/42a5444d52f264682c7805bf8117dd884095c476/storage/innobase/include/read0read.ic#L84


            聊完了上面的MVCC基本知识点,我们接下来看看MySQL InnoDB下的当前读
            快照读
            ?

            当前读、快照读

            Coding的哔哔叨叨

            • 当前读
              像select ... lock in share mode(共享锁
              ), select ... for update、update、insert 、delete(排他锁
              )这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新数据,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

            • 快照读
              不加锁
              的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。

            白了MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现
            • 最后想再多啰嗦几句,我理解的普通select语句就是快照读,它们不会对访问的数据加锁。
              只有普通select语句才会创建read view,select ... lock in share mode(共享锁
              ), select ... for update、update、insert 、delete(排他锁
              )这些操作都不会创建快照,他们都是当前读,因为他们都会对当前访问到的数据加锁。



            RR下的MVCC如何保证可重复读,避免幻读

            Coding的哔哔叨叨

            在这个小结内想和大家聊得是RR隔离级别下,MVCC如何保证可重复读。
            我们都知道RC下存在幻读的问题,即在一个事务内读到了别的事务在此期间已提交的数据,在RR下规避了这个问题,这其中是如何实现的?

            先强调一下,在RR级别下,只靠MVCC是是可以实现可重复读,还能防止部分幻读,但是并不是完全防止。

            举个例子说明下:
            我们都知道执行select....where id between 1 and 10 for update,这时候是当前读,如果只有MVCC时,事务A先执行select....where id between 1 and 10 for update,然后事务B这时insert一条数据,id=5,提交事务,之后事务A下再次执行select....where id between 1 and 10 for update,这时由于是当前读,所以会发现多了一条数据,这时就出现了幻读。所以仅靠MVCC是无法完全防止幻读的。
            所以,在RR级别下,innoDB不仅使用了MVCC,还会对锁访问到的数据加record lock(行锁)和gap lock(间隙锁),禁止其他事务在数据间隙间插入数据,来防止幻读。

            RC和RR下read view创建的时机

            Coding的哔哔叨叨

            Read commited
            :innoDB在RC隔离级别下,事务begin后,执行每条select(读操作)时,都会重置read view,即会新创建一个read view。
            Reapeatable Read
            :innoDB在RR隔离级别下,事务begin后,执行第一个select(读操作)时,会生成一个read view,将当前系统中对本事务不可见的其他活跃事务id都是记录起来,并且在此事务一直使用这个read view,直到事务被commit。


            举例加深印象

            Coding的哔哔叨叨


            下面我们举例来把MVCC的所有知识点串一下。
            假设原始数据如下:
            id
            name
            age
            db_row_id
            db_trx_id
            db_roll_ptr
            1
            Bob
            18
            -
            100000x1234


            不积跬步,无以至千里。

            文章有帮助的话,点个转发、在看呗

            谢谢支持哟 (*^__^*)

            END


            👇

            文章转载自Coding的哔哔叨叨,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

            评论