浅析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 数据类型选择原则
May 27 MySQL
MySQL空间数据存储及函数
Sep 25 MySQL
MySQL 分区表中分区键为什么必须是主键的一部分
Mar 17 MySQL
详细聊一聊mysql的树形结构存储以及查询
Apr 05 MySQL
MySQL的存储函数与存储过程的区别解析
Apr 08 MySQL
Golang连接并操作MySQL
Apr 14 MySQL
SQL语句多表联合查询的方法示例
Apr 18 MySQL
mysql中如何用命令创建联合唯一索引
Apr 20 MySQL
MySQL 字符集 character
May 04 MySQL
MySQL transaction事务安全示例讲解
Jun 21 MySQL
数据设计之权限的实现
Aug 05 MySQL
MySQL下载安装配置详细教程 附下载资源
Sep 23 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实现二分查找算法代码分享
2011/06/24 PHP
PHP中使用CURL获取页面title例子
2015/01/07 PHP
php分割合并两个字符串的函数实例
2015/06/19 PHP
PHP编写RESTful接口
2016/02/23 PHP
ecshop适应在PHP7的修改方法解决报错的实现
2016/11/01 PHP
PHP实现表单提交时去除斜杠的方法
2016/12/26 PHP
PHP实现无限极分类的两种方式示例【递归和引用方式】
2019/03/25 PHP
PHP预定义接口――Iterator用法示例
2020/06/05 PHP
EasySlider 基于jQuery功能强大简单易用的滑动门插件
2010/06/11 Javascript
一个可绑定数据源的jQuery数据表格插件
2010/07/17 Javascript
在javascript中关于节点内容加强
2013/04/11 Javascript
javascript实现的平方米、亩、公顷单位换算小程序
2014/08/11 Javascript
node.js中的emitter.emit方法使用说明
2014/12/10 Javascript
javascript 使用for循环时该注意的问题-附问题总结
2015/08/19 Javascript
基于jquery实现省市联动效果
2015/11/23 Javascript
JavaScript简单获取页面图片原始尺寸的方法
2016/06/21 Javascript
基于javascript实现按圆形排列DIV元素(三)
2016/12/02 Javascript
Angular实现表单验证功能
2017/11/13 Javascript
微信小程序实现添加手机联系人功能示例
2017/11/30 Javascript
微信小程序缓存过期时间的使用详情
2019/05/12 Javascript
vue登录页面cookie的使用及页面跳转代码
2019/07/10 Javascript
[01:33:30]DOTA2-DPC中国联赛 正赛 RNG vs Phoenix BO3 第二场 2月5日
2021/03/11 DOTA
Python 文件读写操作实例详解
2014/03/12 Python
在Django框架中编写Contact表单的教程
2015/07/17 Python
python的pdb调试命令的命令整理及实例
2017/07/12 Python
python中import reload __import__的区别详解
2017/10/16 Python
Python跨文件全局变量的实现方法示例
2017/12/10 Python
window7下的python2.7版本和python3.5版本的opencv-python安装过程
2019/10/24 Python
css3 实现滚动条美化效果的实例代码
2021/01/06 HTML / CSS
马来西亚时装购物网站:ZALORA马来西亚
2017/03/14 全球购物
澳大利亚领先的孕妇服装品牌:Mamaway
2018/08/14 全球购物
英国最大的汽车配件在线商店:Euro Car Parts
2019/09/30 全球购物
学生周末回家住宿长期请假条
2014/02/15 职场文书
党旗在我心中演讲稿
2014/09/15 职场文书
公安机关正风肃纪剖析材料
2014/10/10 职场文书
解决Golang time.Parse和time.Format的时区问题
2021/04/29 Golang