当前位置: 首页 > news >正文

MVCC机制解析:提升数据库并发性能的关键

MVCC机制解析:提升数据库并发性能的关键

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

MVCC只在事务隔离级别为读已提交(Read Committed)和可重复读(Repeated Read)下生效。

MVCC是做什么用的

MVCC是为了处理 可重复读读已提交 事务隔离级别下,在同一事务里,多次执行同一SQL查询语句,不会因为其他事务的横插一脚,对数据进行修改后,导致最终得到的结果不一致。如下图所示,事务B的两次查询得到的结果不一样。

请添加图片描述

MVCC是怎么实现的

串行读 这一事务隔离级别里面,为了保证较高的事务隔离性,采用了将所有的操作加锁互斥,将事务的执行变为顺序执行,相当于单线程的方式,以达到其高隔离性。

而mysql在 可重复读已提交读 事务隔离级别下,他的隔离性是借助MVCC机制来保证的,MVCC机制呢,也不是通过加锁互斥来保证隔离性的,是通过 Undo日志版本链Read View机制 实现的。避免了频繁的加锁互斥阻塞。

Undo日志版本链

Undo日志是什么?

undo日志是回滚日志,在mysql对某一数据进行修改更新时,会将其更新前的数据保存的undo回滚日志里,等当事务执行失败时,用来进行数据回滚的。

而什么是undo日志版本链呢?

就是一行数据被多个事务依次修改后,这条数据就会有很多条undo日志,这些undo日志就会通过一个 roll_pointer 字段进行串联起来,会形成这条数据的历史记录版本链,这个就是undo日志版本链。

roll_pointer 字段哪里来的呢?

在mysql数据库里,数据表都会有两个隐藏的字段属性, trx_id事务idroll_pointer回滚指针 ,其中 trx_id 是用来记录操作数据的,而 roll_pointer 则是用来记录指向上一次修改的日志地址。

注意:begin/start transaction这个开始/启动事务命令,并不是一个事务的起点, 而是在执行到第一条更新update、删除delete、插入insert语句时,事务才真正的启动,才会有真正的事务id。

后生成的trx_id要比先生成的trx_id大。

只有InnoDB数据引擎才支持事务。

下图中,每一条数据都是一个undo回滚日志,通过roll_pointer串联起来后,就是undo日志版本链了。

请添加图片描述

如果第二次修改操作将name改为了 王五 ,又因为某些原因需要进行数据回滚,就要拿到roll_pointer里记录的上一次,也就是第一次修改操作的undo回滚日志地址,将name回滚为 赵六

Read View机制

在可重复读隔离级别下,一个事务在开始时,执行任何的查询SQL脚本,都会生成一个属于当前事务自己的 Read View一致性视图 ,这个视图在这个事务结束之间,都是保持不变的(除非在本事务里面自己执行了更新操作)。

如果事务隔离级别是读已提交,则 Read View一致性视图 是在每次执行查询SQL脚本时,都会重新生成,与可重复读隔离级别的在同一事务里保持不变不同。

这个视图是什么呢?

这个视图是由执行查询SQL脚本这一时刻,所有还未提交的事务id构建而成的数组,和此时存在最大的一个事务id共同构建而成。

示意图一

请添加图片描述

示意图二

请添加图片描述

结合上面两图分析可知,

事务A 第一次执行查询语句时刻,所生成的ReadView的构成为 {[1002,1003,1004], 1004} ,其中[1002,1003,1004]为未提交的事务id(min_id=1002,min_id最小事务id是从未提交事务id里获取的),1004为当前最大的事务id(max_id=1004)。

事务A 第二次、三次执行查询语句时刻,其ReadView的依旧为 {[1002,1003,1004], 1004}

事务B 第一次执行查询语句时刻,所生成的ReadView的构成为 {[1002,1004], 1004} ,其中[1002,1004]为未提交的事务id(min_id=1002),1004为当前最大的事务id(max_id=1004)。

事务B 第二次执行查询语句时刻,所生成的ReadView的依旧为 {[1002,1004], 1004}

根据事务A和事务B的ReadView可以得出一个相同的工具图(用来判断某事务的某一次数据更新,是否对select是可见的)

事务里的select语句查询结果都是需要从undo日志版本链最新数据开始,逐条与本事务的ReadView进行比对,判断应该获取到哪一日志版本的数据为select语句的查询结果。

示意图三

请添加图片描述

比对规则

  • 如果比较的undo日志的 trx_id小于min_id ,则表示这个版本事务是已经提交的,代表本次select这一事务可以查到这个数据。

  • 如果比较的undo日志的 trx_id大于max_id ,则表示这个版本事务是在本次select这一事务后面新启动的,这种数据肯定是不可被查询到的。

  • 如果比较的undo日志的 trx_id大于等于min_id ,且 trx_id小于等于max_id ,则再判断 trx_id是否是在未提交事务id数组里。

    • 在未提交事务id数组里,则表示这个版本数据是由未提交的事务所生成的,这种数据不可被查询到(除非这个未提交的事务就是自己)。
    • 不在未提交事务id数组里,则表示这个版本数据是由已经提交的事务所生成的,这种数据可以被查询到。

案例一

那么事务A第一条select语句查询脚本执行时,获取到的name是什么呢?

先说结果,查询到的name为赵六。

事务A执行第一条select语句时,他的SQL执行顺序,和SQL查询脚本执行时拿到的日志版本链及判断示意图如下。

请添加图片描述

请添加图片描述

上面在示意图二里也说了,事务A的第一次执行查询语句时刻时,其ReadView为 {[1002,1003,1004], 1004}

请添加图片描述

比较步骤如下

第一次: 事务A从undo日志版本链拿到最后一次更新记录(第二次修改记录),得到trx_id为1003,然后对照着上面的 判断工具图 进行比较,大于min_id,小于max_id,所以1003事务id属于是 第二部分 ,且1003这一事务id处于还未提交的事务id构建而成的数组里(未提交的事务id数组[1002,1003,1004]),所以这个事务进行的更新数据,是不可被查询到的。

于是根据roll_pinter回滚指针(上一版本数据的地址)找到上一个版本数据。

第二次: 事务A从undo日志版本链拿到第二条日志记录(第一次插入记录),得到trx_id为1001,然后同样对照着上面的 判断工具图 进行比较,发现小于min_id的1002,所以1001事务id属于是 第一部分 ,是已经提交了事务的,他更新的数据就属于可以被查询到,于是此时查询到的结果name为张三。

案例二

那么事务A第二条select语句查询脚本执行时,获取到的name是什么呢?

先说结果,查询到的name依旧为赵六。

事务A执行第二条select语句时,他的SQL执行顺序,和SQL查询脚本执行时拿到的日志版本链及判断示意图如下。

请添加图片描述

请添加图片描述

上面在示意图二里也说了,事务A的第二次和第三次执行查询语句时刻时,其ReadView依旧为 {[1002,1003,1004], 1004}

请添加图片描述

比较步骤如下

第一次: 事务A从undo日志版本链拿到最后一次更新记录(第四次修改记录),得到trx_id为1003,然后对照着上面的 判断工具图 进行比较,大于min_id,小于max_id,所以1003事务id属于是 第二部分 ,继续判断得知,1003这一事务id处于还未提交的事务id构建而成的数组里(未提交的事务id数组[1002,1003,1004])(即便此时事务1003已经提交了,但是只要在事务A第一次执行查询语句时,这个事务没有提交,那他在事务A里就一直被标记的是未提交,否则就会出现 不可重复读 问题),所以这个事务进行的更新数据,是不可被查询到的。

于是根据roll_pinter回滚指针(上一版本数据的地址)找到上一个版本数据。

第二次: 事务A从undo日志版本链拿到第二条日志记录(第三次修改记录),得到trx_id为1002,然后同样对照着上面的 判断工具图 进行比较,发现等于min_id的1002,所以1002事务id属于是 第二部分 ,继续判断得知,1002这一事务id处于还未提交的事务id构建而成的数组里(未提交的事务id数组[1002,1003,1004]),所以这个事务进行的更新数据,是不可被查询到的。

于是根据roll_pinter回滚指针(上一版本数据的地址)找到上一个版本数据。

第三次: 事务A从undo日志版本链拿到最后一次更新记录(第二次修改记录),得到trx_id为1003,然后对照着上面的 判断工具图 进行比较,大于min_id,小于max_id,所以1003事务id属于是 第二部分 ,且1003这一事务id处于还未提交的事务id构建而成的数组里(未提交的事务id数组[1002,1003,1004]),所以这个事务进行的更新数据,是不可被查询到的。

于是根据roll_pinter回滚指针(上一版本数据的地址)找到上一个版本数据。

第四次: 事务A从undo日志版本链拿到第二条日志记录(第一次插入记录),得到trx_id为1001,然后同样对照着上面的 判断工具图 进行比较,发现小于min_id的1002,所以1001事务id属于是 第一部分 ,是已经提交了事务的,他更新的数据就属于可以被查询到,于是此时查询到的结果name为张三。

案例三

那么事务B第一条select语句查询脚本执行时,获取到的name是什么呢?

先说结果,查询到的name为李四。

为什么事务B的一条查询语句和事务A的第二条查询语句是同时执行的,但是结果不一样呢?可以详细看看下面对事务的比较步骤。

事务B 第一次执行查询语句时刻,其所生成的ReadView的构成为 {[1002,1004], 1004} ,其中[1002,1004]为未提交的事务id(min_id=1002),1004为当前最大的事务id(max_id=1004)。

事务B执行第一条select语句时,他的SQL执行顺序,和SQL查询脚本执行时拿到的日志版本链及判断示意图同案例二里的一样。

比较步骤如下

第一次: 事务B从undo日志版本链拿到最后一次更新记录(第四次修改记录),得到trx_id为1003,然后对照着上面的 案例二的判断工具图 进行比较,大于min_id,小于max_id,所以1003事务id属于是 第二部分 ,继续判断得知,1003这一事务id 不处于 还未提交的事务id构建而成的数组里(未提交的事务id数组[1002,1004]),所以表示这个事务已经提交了,这个事务所更新的数据版本可以被查询到,查询结果就是name为李四。

案例四

事务A第三条select语句查询脚本执行时,获取到的name是什么呢?

先说结果,查询到的name还是赵六。

事务A执行第三条select语句时,他的SQL执行顺序,和SQL查询脚本执行时拿到的日志版本链及判断示意图如下。

请添加图片描述

请添加图片描述

上面在示意图二里也说了,事务A的第二次和第三次执行查询语句时刻时,其ReadView依旧为 {[1002,1003,1004], 1004}

请添加图片描述

比较步骤如下

第一次: 事务A从undo日志版本链拿到最后一次更新记录(第五次修改记录),得到trx_id为1004,然后对照着上面的 判断工具图 进行比较,大于min_id,小于 等于 max_id,所以1004事务id属于是 第二部分 ,且1004这一事务id 不处于 还未提交的事务id构建而成的数组里(未提交的事务id数组[1002,1003,1004]),所以这个事务进行的更新数据,是不可被查询到的。

于是根据roll_pinter回滚指针(上一版本数据的地址)找到上一个版本数据。

第二次: 比较过程和示例二里的第一次比较操作过程一样。

于是根据roll_pinter回滚指针(上一版本数据的地址)找到上一个版本数据。

第三次: 比较过程和示例二里的第二次比较操作过程一样。

于是根据roll_pinter回滚指针(上一版本数据的地址)找到上一个版本数据。

第四次: 比较过程和示例二里的第三次比较操作过程一样。

于是根据roll_pinter回滚指针(上一版本数据的地址)找到上一个版本数据。

第五次: 比较过程和示例二里的第四次比较操作过程一样。得到查询结果name为张三。

案例五

那么事务B第二条select语句查询脚本执行时,获取到的name是什么呢?

先说结果,查询到的name为李四。

事务B 第二次执行查询语句时刻,其所生成的ReadView的依旧为 {[1002,1004], 1004} 。和其第一次执行查询语句时刻一样。

事务B执行第三条select语句时,他的SQL执行顺序图,和SQL查询脚本执行时拿到的日志版本链和上面案例三的一样。

虽然ReadView和案例三的不同,为 {[1002,1004], 1004} ,但是其形成的判断图还是一样的。

请添加图片描述

比较步骤如下

第一次: 事务B从undo日志版本链拿到最后一次更新记录(第五次修改记录),得到trx_id为1004,然后对照着上面的 判断工具图 进行比较,大于min_id,小于 等于 max_id,所以1004事务id属于是 第二部分 ,且1004这一事务id 不处于 还未提交的事务id构建而成的数组里(未提交的事务id数组[1002,1004])(即便此时事务1004已经提交了,但是只要在事务B第一次执行查询语句时,这个事务没有提交,那他在事务B里就一直被标记的是未提交,否则就会出现 不可重复读 问题),所以这个事务进行的更新数据,是不可被查询到的。(和上面案例二的第一次比较一样)

第二次:事务B从undo日志版本链拿到第二条日志记录(第四次修改记录),得到trx_id为1003,然后对照着上面的 案例二的判断工具图 进行比较,大于min_id,小于max_id,所以1003事务id属于是 第二部分 ,继续判断得知,1003这一事务id 不处于 还未提交的事务id构建而成的数组里(未提交的事务id数组[1002,1004]),所以表示这个事务已经提交了,这个事务所更新的数据版本可以被查询到,查询结果就是name为李四。

读已提交隔离级别又是怎么比较的

读已提交 (已提交读)事务隔离级别的undo日志版本链和 可重复读 是一样的,Read View的生成规则就不同了, 读已提交的 Read View一致性视图 是在每次执行查询SQL脚本时,都会重新生成 ,与可重复读隔离级别的在同一事务里保持不变的定义不同。

这也就导致每次执行查询脚本时,都会重新构建 还未提交的事务id数组 ,所以只要其他事务在本事务执行这一条查询脚本之前,更新数据并提交了,那他的更新数据就可以被本事务的这一次查询操作查询到。

其他的比对规则还是和 可重复读 事务隔离级别一样,仅仅是ReadView在每次查询时需要重新生成。

可以理解为 读已提交 事务隔离,只会读到最后一次提交了更新操作事务的数据。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • LabVIEW减速机加载控制系统
  • SpringCloud-04 OpenFeign服务调用与负载均衡
  • Springboot 集成 Swing
  • 【面试八股总结】GMP模型
  • 小程序开发设计-第一个小程序:创建小程序项目④
  • curl格式化json之jq工具?
  • Java高级编程——泛型(泛型类、泛型接口、泛型方法,完成详解,并附有案例+代码)
  • 6--SpringBootWeb案例(详解)
  • springboot实战学习笔记(2)
  • vue项目中,属性表头默认为横向排列,却没有纵向排列的组件或相关属性,如何将其纵向排列?
  • vivado中选中bd文件后generate output product是什么用,create HDL wrapper是什么用
  • Java | Leetcode Java题解之第414题第三大的数
  • 检查和测绘室内防撞无人机技术详解
  • 828华为云征文|Flexus X实例Docker+Jenkins+gitee实现CI/CD自动化部署-解放你的双手~
  • Qt 模型视图(四):代理类QAbstractItemDelegate
  • 分享的文章《人生如棋》
  • css属性的继承、初识值、计算值、当前值、应用值
  • ESLint简单操作
  • leetcode讲解--894. All Possible Full Binary Trees
  • node-sass 安装卡在 node scripts/install.js 解决办法
  • React-flux杂记
  • SQLServer之索引简介
  • TypeScript实现数据结构(一)栈,队列,链表
  • VUE es6技巧写法(持续更新中~~~)
  • vue.js框架原理浅析
  • Vue.js源码(2):初探List Rendering
  • vue的全局变量和全局拦截请求器
  • 阿里研究院入选中国企业智库系统影响力榜
  • 如何合理的规划jvm性能调优
  • 通过git安装npm私有模块
  • 在 Chrome DevTools 中调试 JavaScript 入门
  • scrapy中间件源码分析及常用中间件大全
  • UI设计初学者应该如何入门?
  • ​【数据结构与算法】冒泡排序:简单易懂的排序算法解析
  • ​【原创】基于SSM的酒店预约管理系统(酒店管理系统毕业设计)
  • ​软考-高级-信息系统项目管理师教程 第四版【第14章-项目沟通管理-思维导图】​
  • #define
  • #if和#ifdef区别
  • #前后端分离# 头条发布系统
  • (007)XHTML文档之标题——h1~h6
  • (1)Jupyter Notebook 下载及安装
  • (C语言)逆序输出字符串
  • (Mirage系列之二)VMware Horizon Mirage的经典用户用例及真实案例分析
  • (阿里云在线播放)基于SpringBoot+Vue前后端分离的在线教育平台项目
  • (八)Spring源码解析:Spring MVC
  • (附源码)springboot课程在线考试系统 毕业设计 655127
  • (附源码)ssm失物招领系统 毕业设计 182317
  • (六)c52学习之旅-独立按键
  • (三分钟)速览传统边缘检测算子
  • (一)VirtualBox安装增强功能
  • (一)模式识别——基于SVM的道路分割实验(附资源)
  • .Net 中的反射(动态创建类型实例) - Part.4(转自http://www.tracefact.net/CLR-and-Framework/Reflection-Part4.aspx)...
  • .NET/C# 使用反射注册事件
  • .NET国产化改造探索(三)、银河麒麟安装.NET 8环境
  • .NET之C#编程:懒汉模式的终结,单例模式的正确打开方式