浅析MySQL如何实现事务隔离


Posted in MySQL onJune 26, 2021

一、前言

众所周知,MySQL的在RR隔离级别下查询数据,是可以保证数据不受其它事物影响,而在RC隔离级别下只要其它事物commit后,数据都会读到commit之后的数据,那么事物隔离的原理是什么?是通过什么实现的呢?那肯定是通过MVCC机制(Multi-Version Concurrency Control,即多版本并发控制)。

注:MySQL的InnoDB引擎之所以能够支持高性能的并发性能,就是由于MySQL的MVCC机制(归功于undo log、Read-View、),但是本篇不对MVCC过多的介绍。

参考资料:《MySQL实战45讲》系列,虽然讲解的比较清晰,但是仍然需要理解,比如关于视图数组部分我认为是相比较而言没有解释清楚,所以结合资料与自己见解加以记录!

二、RC与RR隔离级别

我们分别开启RC与RR隔离级别实验说明,首先假设有account账户表,在事务ABC开启前,账户中的余额balance为1,即

select balance from account =1; # 结果为1

2.1、RR事务隔离级别下查询结果

当在RR事务隔离级别分别开启三个事务,在不同时间段内做如下操作

  • 事务A(显式开启事务,手动commit提交):查询余额
  • 事务B(显式开启事务,手动commit提交):对id=1的余额加1
  • 事务C(不显式开启事务,自动提交):对id=1的余额加1

浅析MySQL如何实现事务隔离

我们从时间逻辑上分为三个阶段,分析结果

  • 第一阶段:事务A立马开始事务,随后事务B也紧跟着立马开始事务,然后事务C首先更新balance为2成功,当前balance=2;
  • 第二阶段:事务B更新balance的值,此时先读到当前balance最新值为2,随后set balance=balance+1成功,当前balance=3;
  • 第三阶段:事务A查询balance的值,此时的值为1(这里为什么等于1呢,是怎么实现的呢?不应该是当前最新值3吗?这就是本篇博文讨论的重点),最后commit结束事务,紧接着事务B也commit结束事务

最后事务A读取balance的结果是1,理所当然,RR即为可重复读,即一个事务在执行过程中看到的数据,总是跟这个事务启动时看到的数据是一致的,当前事务不管有没有提交,都不会影响数据,我只需要读取基于快照的数据即可,这就是快照读。但是我们要讨论的是如何在MVCC机制下实现?

注:begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用start transaction with consistent snapshot 这个命令。

2.2、RC事务隔离级别下查询结果

同样地,我们在RC隔离下,开启事务ABC,观察事务A最后的balance结果。

浅析MySQL如何实现事务隔离

最后事务A读取balance的结果是2,理所当然,RC即为读可提交,字面意思就是其他事务只要提交后,当前事务我就能立马读取到最新当前值,这就是当前读。但是我们要讨论的是如何在MVCC机制下实现?

实际上这是因为实现MVCC时用到的一致性读视图,即consistent read view,用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现。

三、事务隔离在MVCC的实现

在探讨MVCC如何实现事务隔离前,我们需要知道是视图数组、一致性视图等概念,才能帮助更好理解MVCC帮助事务实现了隔离。

3.1、数据行ROW的多版本

InnoDB里面每个事务有一个唯一的事务ID,叫作transaction id。它是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的。

而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把transaction id赋值给这个数据版本的事务ID,记为row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它(通过undo_log文件找到)。

也就是说,数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id。

对某一个数据行ROW某个时刻经过三次更新事务的多版本控制流程,画如下图加深理解。

浅析MySQL如何实现事务隔离

从图我们可以得到:

  • ROW有四个版本V1-V4,即经过三次更新balance后,当前最新版本为V4,当前balance已经更新为4,是最新值
  • InnoDB每次更新事务产生的transaction id都会赋值给row trx_id;
  • 通过undo_log可以从V4撤回到V1,找到V1版本的balance=1,即undo_log回滚版本。

明白了数据行的ROW的多版本原理与实现后,可以帮助我们理解InnoDB是怎么定义并创建快照的!

3.2、视图数组

下述部分出自资料中的原句,特别是红色加深部分可能会比较难以理解,所以需要结合自己理解并画图

InnoDB是这么在事务开启的时候定义快照的,哪些事务的操作我可以忽视,哪么我必须要保存在快照里。可以理解为:一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。

在实现上, InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。“活跃”指的就是,启动了但还没提交。数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位。这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。

浅析MySQL如何实现事务隔离

我对低水位与高水位的理解:

低水位=当前所有启动了但未提交事务集合的ID最小值=当前事务的上一个启动但未提交的事务ID最小值(所有活跃事务ID最小值)

高水位=当前事务的ID(当前ROW版本号/row trx_id)=已经创建过事务ID的最大值+1

举例说明:仍然以上述RR隔离级别下三个ABC事务为例

  • 事务A开始前,系统里面只有一个活跃事务ID是99;
  • 事务A、B、C的版本号分别是100、101、102,且当前系统里只有这四个事务;
  • 三个事务开始前,(id,balance)=(1,1)这一行数据的row trx_id是90。

这样,事务A的视图数组就是[99], 事务B的视图数组是[99,100], 事务C的视图数组是[99,100,101]。即视图数组通用公式为:[{当前事务开启瞬间活跃事务ID集合}]。

而数据版本的可见性规则,就是基于rowtrx_id和一致性视图对比结果得到的,所以我们还必须再了解下一致性视图

3.3、一致性视图

通过对视图数组的理解,一致性视图就更加容易了,即:这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。

仍然以上述RR隔离级别下三个ABC事务为例

  • 事务A开始前,系统里面只有一个活跃事务ID是99, 所以事物A开启瞬间活跃事物集合为[99];
  • 事务A、B、C的版本号分别是100、101、102,且当前系统里只有这四个事务,所以事物A、B、C高水位分别为100、101、102;
  • 三个事务开始前,(id,balance)=(1,1)这一行数据的row trx_id是90。

这样,事务A的一致性视图就是[99,100], 事务B的一致性视图是[99,100,101], 事务C的一致性视图是[99,100,101,102]。即一致性视图通用公式为:[{当前事务开启瞬间活跃事务ID集合},当前row trx_id]。

浅析MySQL如何实现事务隔离

分析上述流程图结果:

第一个有效更新版本是事物C,更新balance=2,这个时候的最新版本rowtrx_id=102,而之前的在事物ABC之前的活跃事物最新版本row trx_id为99,所以此时99已经成为历史版本1;

第二个有效更新版本是事物B,更新balance=3,这个时候最新版本rowtrx_id=101,而此时row trx_id=102成为历史版本1,而rowtrx_id=99成为历史版本2;

事物A查询的时候,事物B是没有提交,但生成的(id, balance)=(1, 3)已经成为当前最新版本,事物A读取数据时,一致性视图为[99, 100],而读数据都是从当前版本切的然后对比row trx_id,所以会有以下流程:

  • 找到(1,3)的时候,判断出row trx_id=101,比高水位大,处于红色区域,不可见;
  • 接着,找到上一个历史版本,一看row trx_id=102,比高水位大,处于红色区域,不可见;
  • 再往前找,终于找到了(1,1),它的row trx_id=90,比低水位小,处于绿色区域,可见。

最后事物A无论在什么时候查询,看到的数据都是一致性视图[99, 100]生成的快照数据(1, 1),即rowtrx_id=90时的数据。这就称之为一致性读。

总结:

对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:

  • 版本未提交,不可见;
  • 版本已提交,但是是在视图创建后提交的,不可见;
  • 版本已提交,而且是在视图创建前提交的,可见。

现在,我们用这个规则来判断图中的查询结果,事务A的查询语句的视图数组是在事务A启动的时候生成的,这时候:

  • (1,3)还没提交,属于情况1,不可见;
  • (1,2)虽然提交了,但是是在视图数组创建之后提交的,属于情况2,不可见;
  • (1,1)是在视图数组创建之前提交的,可见。

3.4、当前读与快照读

3.4.1、当前读与快照读规则

当然按照这个一致性读的逻辑,事物B在事物C有效更新balance=2之后,但是事物B的视图数组是在事物C生成的,所以理论上来说不应该是事物B看到的是(id, balance)=(1, 1)这个数据(快照/历史版本)吗?而看不到当前版本(1, 2)数据。为什么事物B在更新balance之后直接数据就成为(1, 3)了呢?

如果事物B在update之前select一次数据,看到的值确实是balance=1,但是update是不能在历史版本上操作的,否则事物C的更新就会丢失,所以update操作都是在先读取当前版本,然后再更新。

也就说有这么一条规则:更新数据都是先读后更新,而这个读是读当前最新值,称之为“当前读(currentread),而只查询不读的话就会读取当前快照,称之为“快照读”。所以在事物B更新balance之前,先查询到最新的版本(1, 2)然后再更新为(1, 3)。而事物A查询的快照数据为(1, 1),而不是最新版本(1, 3)。

3.4.2、当前读与快照读解释

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

快照读:像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。是基于多版本控制的,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本(快照数据)。

3.4.3、RC读可提交下的视图规则

读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:

在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询,都共用这个一致性视图;在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图,此时start transaction with consistent snapshot就等同于普通的starttransaction/begin所以在RC隔离级别下,事物A与事物B查询到的数据分别如下:

浅析MySQL如何实现事务隔离

事物C立马更新balance=2,然后自动提交,生成最新版本(1, 2),此时重新计算出视图数据(1, 2);事物B查到此时的最新版本为(1, 2),之后再更新为版本(1, 3)为当前最新版本,查询此时的事物B select到的balance=3(事物B更新balance=3之后立马算出一个新的视图,select就是根据此视图得到的数据),而不是1。而此时事物B还未提交,对于事物A来说是看不见的,所以事物A此时读取到的事物C提交的最新版本(1, 2)。

以上就是浅析MySQL如何实现事务隔离的详细内容,更多关于MySQL事务隔离的资料请关注三水点靠木其它相关文章!

MySQL 相关文章推荐
mysql优化
Apr 06 MySQL
MySQL Shell的介绍以及安装
Apr 24 MySQL
Mysql文件存储图文详解
Jun 01 MySQL
MySQL8.0无法启动3534的解决方法
Jun 03 MySQL
MySQL为id选择合适的数据类型
Jun 07 MySQL
安装配置mysql及Navicat prenium的详细流程
Jun 10 MySQL
mysql联合索引的使用规则
Jun 23 MySQL
SQL IDENTITY_INSERT作用案例详解
Aug 23 MySQL
MySQL数据库超时设置配置的方法实例
Oct 15 MySQL
详解MySql中InnoDB存储引擎中的各种锁
Feb 12 MySQL
深入讲解数据库中Decimal类型的使用以及实现方法
Feb 15 MySQL
MySQL 字符集 character
May 04 MySQL
MySQL开启事务的方式
MySQL中IF()、IFNULL()、NULLIF()、ISNULL()函数的使用详解
Jun 26 #MySQL
解决mysql问题:由于找不到MSVCR120.dll,无法继续执行代码
解决mysql:ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO/YES)
Jun 26 #MySQL
MySQL的安装与配置详细教程
浅谈MySQL之浅入深出页原理
MySql 8.0及对应驱动包匹配的注意点说明
Jun 23 #MySQL
You might like
PHP中unset,array_splice删除数组中元素的区别
2014/07/28 PHP
jQuery 隔行换色 支持键盘上下键,按Enter选定值
2009/08/02 Javascript
jquery预览图片实现鼠标放上去显示实际大小
2014/01/16 Javascript
angular中使用路由和$location切换视图
2015/01/23 Javascript
一道JS前端闭包面试题解析
2015/12/25 Javascript
高性能JavaScript循环语句和条件语句
2016/01/20 Javascript
在javascript中使用com组件的简单实现方法
2016/08/17 Javascript
Bootstrap 轮播(Carousel)插件
2016/12/26 Javascript
Vue.js对象转换实例
2017/06/07 Javascript
layui中layer前端组件实现图片显示功能的方法分析
2017/10/13 Javascript
探讨Vue.js的组件和模板
2017/10/27 Javascript
webpack+vue中使用别名路径引用静态图片地址
2017/11/20 Javascript
解决Nodejs全局安装模块后找不到命令的问题
2018/05/15 NodeJs
Vue 与 Vuex 的第一次接触遇到的坑
2018/08/16 Javascript
JS拖动选择table里的单元格完整实例【基于jQuery】
2019/05/28 jQuery
基于scrapy实现的简单蜘蛛采集程序
2015/04/17 Python
在Python中操作时间之strptime()方法的使用
2020/12/30 Python
django rest framework之请求与响应(详解)
2017/11/06 Python
快速解决PyCharm无法引用matplotlib的问题
2018/05/24 Python
NumPy.npy与pandas DataFrame的实例讲解
2018/07/09 Python
Python迷宫生成和迷宫破解算法实例
2019/12/24 Python
Pytorch使用MNIST数据集实现基础GAN和DCGAN详解
2020/01/10 Python
Python3 实现爬取网站下所有URL方式
2020/01/16 Python
解决python gdal投影坐标系转换的问题
2020/01/17 Python
Tensorflow安装问题: Could not find a version that satisfies the requirement tensorflow
2020/04/20 Python
TensorFlow-gpu和opencv安装详细教程
2020/06/30 Python
利用html5 canvas破解简单验证码及getImageData接口应用
2013/01/25 HTML / CSS
ProBikeKit美国官网:自行车套件,跑步和铁人三项套件
2016/10/13 全球购物
美国家居装饰网上商店:Lulu & Georgia
2019/09/14 全球购物
国际经济贸易专业推荐信
2013/11/06 职场文书
门卫工作岗位职责
2013/12/17 职场文书
求职简历自荐信
2014/06/18 职场文书
社区党员公开承诺书
2014/08/30 职场文书
2015年小学数学教师个人工作总结
2015/05/25 职场文书
教你如何使用Python实现二叉树结构及三种遍历
2021/06/18 Python
详解Python内置模块Collections
2022/03/22 Python