浅析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集群搭建
May 26 MySQL
MySQL中使用or、in与union all在查询命令下的效率对比
May 26 MySQL
详解MySQL多版本并发控制机制(MVCC)源码
Jun 23 MySQL
为什么MySQL选择Repeatable Read作为默认隔离级别
Jul 26 MySQL
SQL语法CONSTRAINT约束操作详情
Jan 18 MySQL
JMeter对MySQL数据库进行压力测试的实现步骤
Jan 22 MySQL
MySQL事务操作的四大特性以及并发事务问题
Apr 12 MySQL
MySQL聚簇索引和非聚簇索引的区别详情
Jun 14 MySQL
MySQL串行化隔离级别(间隙锁实现)
Jun 16 MySQL
MySQL表字段数量限制及行大小限制详情
Jul 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
php4的彩蛋
2006/10/09 PHP
php函数重载的替代方法--伪重载详解
2015/05/08 PHP
php获取文件类型和文件信息的方法
2015/07/10 PHP
简单的pgsql pdo php操作类实现代码
2016/08/25 PHP
PHP7.1新功能之Nullable Type用法分析
2016/09/26 PHP
php获取数据库中数据的实现方法
2017/06/01 PHP
[原创]IE view-source 无法查看看源码 JavaScript看网页源码
2009/07/19 Javascript
JS+CSS制作DIV层可(最小化/拖拽/排序)功能实现代码
2013/02/25 Javascript
Jquery多选下拉列表插件jquery multiselect功能介绍及使用
2013/05/24 Javascript
完美解决IE低版本不支持call与apply的问题
2013/12/05 Javascript
JS验证邮箱格式是否正确的代码
2013/12/05 Javascript
Jquery 过滤器(first,last,not,even,odd)的使用
2014/01/22 Javascript
javascript日期处理函数,性能优化批处理
2015/09/06 Javascript
浅谈Nodejs观察者模式
2015/10/13 NodeJs
微信小程序 setData的使用方法详解
2017/04/20 Javascript
关于HTTP传输中gzip压缩的秘密探索分析
2018/01/12 Javascript
vue iview实现动态路由和权限验证功能
2018/04/17 Javascript
jQuery实现ajax回调函数带入参数的方法示例
2018/06/26 jQuery
快速解决select2在bootstrap模态框中下拉框隐藏的问题
2018/08/10 Javascript
bootstrap模态框关闭后清除模态框的数据方法
2018/08/10 Javascript
详解JavaScript之Array.reduce源码解读
2020/11/01 Javascript
你不知道的SpringBoot与Vue部署解决方案
2020/11/09 Javascript
Python import自定义模块方法
2015/02/12 Python
Python自动化开发学习之三级菜单制作
2017/07/14 Python
django session完成状态保持的方法
2018/11/27 Python
python实现飞船大战
2020/04/24 Python
在weblogic中发布ejb需涉及到哪些配置文件
2012/01/17 面试题
经济职业学院毕业生自荐书
2014/03/17 职场文书
工地门卫岗位职责范本
2014/07/01 职场文书
学习礼仪心得体会
2014/09/01 职场文书
2014年机关党委工作总结
2014/12/11 职场文书
模范教师材料大全
2014/12/16 职场文书
财务人员个人工作总结
2015/02/27 职场文书
党员转正大会主持词
2015/07/02 职场文书
MySQL利用UNION连接2个查询排序失效详解
2021/11/20 MySQL
FFmpeg视频处理入门教程(新手必看)
2022/01/22 杂记