• MySQL的MVCC机制理解和总结


    关于MySQL的MVCC,网上的资料很多,也比较杂,本文是整理了相关资料后,结合我个人的理解所做的总结。

    什么是MVCC

    MVCC是多版本并发控制,可以在写数据的同时,支持并发读数据,从而提升并发性能。

    讨论MVCC的两个前提条件

    • MVCC只在RC(READ COMMITTED)和RR(REPEATABLE READ)两个事务隔离级别下工作
    • MVCC中的读是快照读,不是当前读

    MVCC的几个概念

    • 隐藏字段trx_id和roll_pointer

    InnoDB存储引擎的聚簇索引中有两个隐藏列:trx_id和roll_pointer

    trx_id是事务版本号,每次有事务更改聚簇索引的记录时,会生成一个当前事务的id,并且把当前事务id赋值给trx_id列。

    roll_pointer是回滚指针,每次有事务更改聚簇索引的记录时,会把旧版本数据写入到undo日志中,通过回滚指针指向undo日志的位置就可以找到修改前的数据。

    • 版本链
      每次更改聚簇索引的记录,都会记录一条undo日志,每条undo日志也都有一个roll_pointer,因此,把这些roll_pointer串起来形成一个链表,就是版本链。
      借用掘金小册《MySQL是怎样运行的:从根儿上理解MySQL》的图来看,就很清楚什么是版本链了
      在这里插入图片描述

    • ReadView
      ReadView是一致性读视图,它是当前数据和undo日志中数据的一个快照,用来判断查询数据时,哪个版本的数据可以被查到。

    ReadView有4个属性:

    1. m_ids:表示在生成ReadView时当前系统中活跃的读写事务(也就是还未提交的事务)的事务id列表。
    2. min_trx_id:m_ids中的最小值
    3. max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id。不是m_ids中的最大值,而是m_ids中的最大值+1
    4. creator_trx_id:生成该ReadView的事务的事务id

    有了版本链和ReadView,MySQL就可以实现MVCC了,具体过程如下。

    MVCC的机制

    查询某一条数据时(前提一定是快照读),假如这条数据在ReadView里有多个版本,那么先取版本链头部的那条数据的trx_id字段,也就是上次修改了该条数据的事务id,然后按照这几个步骤判断应该能查到哪个版本的数据:

    1. 如果trx_id==creator_trx_id,说明当前事务访问的是自己修改过的数据,所以这条数据对当前查询可见。
    2. 如果trx_id
    3. 如果trx_id>=max_trx_id,说明上次修改了该条数据的事务是在生成ReadView之后才开启的,这条数据不可见,然后从版本链中取这条数据的下一个版本,用下一个版本的trx_id从步骤1开始再次判断。
    4. 如果 min_trx_id<=trx_id

    经过以上步骤,就能确定版本链中哪个版本的数据对当前查询可见。如果版本链里所有版本的数据都不可见,那么就查不到该条数据。

    对于RC隔离级别来说,每次读取数据前都生成ReadView
    对于RR隔离级别来说,第一次读取数据时生成ReadView,同一个事务中多次读取数据使用同一个ReadView

    二级索引和MVCC

    MVCC的机制是依赖聚簇索引的隐藏字段trx_id和roll_pointer,但是二级索引并没有这两个隐藏列,无法直接用二级索引实现MVCC。

    二级索引的页Page Header中有一个 MAX_TRX_ID属性,表示最后一次更新二级索引的事务id。如果MAX_TRX_ID=ReadView的min_trx_id,则回表到聚簇索引,用聚簇索引的MVCC机制判断可见性。

    几种错误的理解

    • begin之后就会开启事务,并获得事务id

    正确的理解是:begin语句用来显示开启一个事务,但并不是在这时候获得事务id。
    可以用这个方法来实验:
    在这里插入图片描述
    从INNODB_TRX中并不会查到任何事务,此时不会获得事务id。

    • begin之后第一个select语句会开启事务,并获得事务id

    正确的理解是:begin之后的select会开启一个事务,但这个事务并没有对数据做增删改的操作,不会分配事务id,事务id实际上为0。
    先看一个begin之后update的例子:
    在这里插入图片描述
    这是正常的一个事务,事务id是5677,事务状态是running。
    再看一个begin之后select的例子:
    在这里插入图片描述
    这也是一个事务,但它的事务id非常大。实际上,这个事务的事务id是0,非常大的数字是显示事务id时计算出来的数字,计算方式为当前事务的trx变量的指针地址转成整数,再加上2^48。
    在MVCC中,这个查询语句生成的ReadView的creator_trx_id=0

    • 每个事务开启后都会得到一个ReadView

    正确的理解是:生成ReadView的时机是select快照读查询,而不是开启事务

    • 如果一个事务第二次查询和第一次查询的结果不一致,说明存在幻读

    正确的理解是:一个事务第二次查询的结果比第一次查询的结果多,说明存在幻读。幻读强调的是第二次查到了第一次没查到的数据,两次查询的数据量增加了。这是幻读和不可重复读的差别。不可重复读强调的是同一条数据,前后两次查询的结果不同。

    • RR隔离级别下,同一个事务内使用的是同一个ReadView,因此可以解决幻读问题。

    正确的理解是,虽然RR隔离级别下同一个事务内用的是同一个ReadView,但如果事务内更新了数据,再次查询时是可能出现幻读情况的,举个反例:

    有一个城市表city,初始有两条数据
    在这里插入图片描述
    事务隔离级别为默认的RR
    在这里插入图片描述

    按以下次序,有两个会话分别读写该表

    时序session1session2
    T1begin;
    T2select * from city where province=‘广东’;
    T3begin; insert into city values(5,‘珠海’,‘广东’); commit;
    T4select * from city where province=‘广东’;
    T5update city set name=‘深圳’ where province=‘广东’;
    T6select * from city where province=‘广东’;

    T4时刻session1的查询和T2时刻的查询在同一个事务中,用的是同一个ReadView,此时查不到sesstion2新增的数据。此时查询结果是深圳和广州两条数据。
    T5时刻更新数据时,更新了3条数据
    T6时刻再次查询,会把最新的3条数据同时查出来,此时出现了幻读
    在这里插入图片描述


    参考资料

    掘金小册《MySQL是怎样运行的:从根儿上理解MySQL》
    《MVCC详解,深入浅出简单易懂》https://blog.csdn.net/lans_g/article/details/124232192
    《看一遍就懂:MVCC原理详解》https://zhuanlan.zhihu.com/p/451830526
    《数据库基础(四)Innodb MVCC实现原理》https://zhuanlan.zhihu.com/p/52977862
    《MySQL的可重复读级别能解决幻读吗》https://cloud.tencent.com/developer/article/1417728
    《InnoDB的ID家族[ROW_ID,XID,TRX-ID,THREAD-ID]》https://segmentfault.com/a/1190000041540497?utm_source=sf-similar-article
    《mysql二级索引没有mvcc_MySQL InnoDB MVCC深度分析》https://blog.csdn.net/weixin_35731579/article/details/113294971
    《InnoDB多版本(MVCC)实现简要分析》https://blog.csdn.net/bluetjs/article/details/52672819

  • 相关阅读:
    SSM+基于SSM的智慧社区宠物医院 毕业设计-附源码211621
    Ajax 获取 JSON数据
    DM 集群软硬件环境需求
    [附源码]java毕业设计基于的疫苗预约系统
    java中(String)类常用方法
    Python实战小项目分享
    Nginx代理websocket为什么要这样做?
    Java的继承
    重生奇迹MU游戏的特别之处
    对接京东平台的第一篇
  • 原文地址:https://blog.csdn.net/liumang361/article/details/133218976