《MySQL实战45讲》——学习笔记08 “一致性视图、可重复读实现“

news/2024/4/29 12:18:12/文章来源:https://blog.csdn.net/minghao0508/article/details/127194650

这篇文章讲的比较分散,这里做一个梳理,先将简单的概念如"事务的启动时机"、"视图"、"秒级创建快照"拎出来解释,然后通过文章中的几个例子说明"一致性读"和"当前读";
 

08 |  事务到底是隔离的还是不隔离的?

事务的启动时机?

  • 第一种启动方式:一致性视图是在执行事务过程中的第一个查询语句时创建的;
  • 第二种启动方式:一致性视图是在执行start transaction with consistent snapshot时创建的;

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

还需要注意的是,默认 autocommit=1下,单独的一条update语句本身就是一个事务,语句完成的时候会自动提交;

MySQL里“视图”的概念

在MySQL里,有两个“视图”的概念:

(1)一个是view;它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果;创建视图的语法是create view…,而它的查询方法与表一样;

(2)另一个是consistent readview,它是InnoDB在实现MVCC时用到的一致性读视图,用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现;它没有物理结构,作用是事务执行期间用来定义“我能看到什么数据”

"快照"在MVCC里是怎么实现的?

InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。

在可重复读隔离级别下,事务在启动的时候就“拍了个快照”,这个快照是基于整库的

但是这里"快照"并不是说把此刻的整库数据拷贝一份,而是类似git,对于每一行数据,仅记录其在某一次事务中的增量更新,因此大部分的数据都是没有更新的,从而不会像"全量物理拷贝"那样占用巨大的内存,具体原理如下:

(1)InnoDB下,每个事务都有一个唯一的事务ID,即transaction id,它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的;

(2)每行数据也是有多个版本的;每次事务更新数据的时候,都会生成这行数据的一个新的数据版本,将这个数据版本对应的事务ID记为row trx_id;

(3)旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它;

也就是说,对于数据表中的一行记录,在数据库的不断更新下,可能存在多个数据版本 (row),而每个数据版本有自己的 row trx_id;如下图所示,就是一条记录被多个事务连续更新后的过程;

由图可知:语句所在的事务ID与语句更新结果的数据版本的row trx_id一一对应;实际上,历史版本数据的值并不会物理存在,而是在每次需要的时候根据当前版本和 undo log 计算出来的

既然有了row trx_id,而事务ID又是有序递增的,再思考下“秒级创建快照”的能力是如何工作的?

按照可重复读的定义,一个事务启动的时候,它能够看到所有已经提交的事务结果;但是之后,在这个事务执行期间,其他事务的更新对它都不可见;

因此,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”;当然,如果“上一个版本”也不可见,那就得继续往前找;这里要注意,如果是这个事务自己更新的数据,它自己还是要认的

作者在这里举了一个不容易理解且容易引起误解的例子,来说明一致性视图(read-view),原文描述如下并配了一张图(我在图里作了补充说明):

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

 这个图里面,不同颜色的区域的描述不准确,这里先说明下几个重要的点

1. 先使用begin命令的事务,其trx_id不一定比后使用begin命令的事务的trx_id小;

这里用的是"先使用begin命令的事务"这样的描述,而不是"先开启的事务",因为上面讲过"begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动";

2. 除非使用start transaction with consistent snapshot命令,一致性视图是在执行第一个快照读语句时创建的

3. 当前事务真正生成trx_id的时机在于"当前读",也就是非select的一般更新DML语句,如update、insert、delete、select...for update;

实际上,如果事务内先执行的第一条语句是读语句,会生成一条临时的trx_id,这个"临时的trx_id"的值并不是有当前数据库最大的trx_id递增生成,而是按照一定的计算规则;但是当后面遇到第一条更新语句时,这个trx_id的值会被更新;可参考这篇文章《mysql中事务id,有啥用?》;

4. 视图数组生成的时机在于事务启动后的第一条读语句,而非事务启动时就立即创建,也不是分配trx_id时创建

基于以上原因,所以视图数组中可能存在这2种事务:

(1)trx_id小于当前事务A,但未提交的事务A_pre:如事务A_pre先执行了一条非select的DML语句,生成trx_id=10,然后执行多条SQL语句,事务A_pre提交之前,当前事务A也开启执行了一条非select的DML语句,生成trx_id=11,此时A又执行了一条查询语句,生成视图数组;
(2)trx_id大于当前事务A,但未提交的事务A_behind:如事务A先执行了一条非select的DML语句,生成trx_id=11,在A执行到第一条读语句之前,事务A_behind也开启执行了一条非select的DML语句,生成trx_id=12,此时A又执行了一条查询语句,生成视图数组;

注意:当使用start transaction with consistent snapshot这个命令开启事务时,会立即生成一致性视图,这种情况下,当前事务ID若已生成,则视图数组的最后一个元素就是当前事务ID,高水位的值就是当前事务ID+1;文中举的例子就是用此方式开启事务;

个人理解这个视图数组是用来判断当前事务对哪些数据版本是可见的,对哪些是不可见的,作者也在文中指出"判断规则是从代码逻辑直接转译过来的",因此步骤应该如下:

首先,确定视图数组;在当前事务执行第一条读语句时,生成一致性视图,创建视图数组,数组中的trx_id元素为当前正在"活跃"的所有事务ID,即启动且生成trx_id但还没提交的事务;

其次,确定文中的"低水位"和"高水位";"低水位"的值为系统中未提交事务trx_id的最小值,也就是视图数组的最小值;"高水位"的值是当前系统内的已经创建过的事务trx_id的最大值加1;

最后,根据当前事务的trx_id,找到其可见的数据版本

  1. 如果当前数据版本的trx_id大于或等于"高水位",则说明这个事务是当前事务创建一致性视图之后才启动的,因此不可见;
  2. 如果当前数据版本的trx_id小于"低水位",则说明这个事务是当前事务创建一致性视图之前就完成提交的,因此可见;
  3. 如果当前数据版本的trx_id大于"高水位"而小于"高水位",则判断其是否在视图数组的元素中(数组不一定连续,因为可能有部分trx_id更大但执行更快的事务已经完成提交),如果在数组内则说明未提交,因此不可见;如果不在数组内则说明已经提交,因此可见;

一致性读

举个例子:

CREATE TABLE `t` ( `id` int(11) NOT NULL, `k` int(11) DEFAULT NULL, PRIMARY KEY (`id`)
) 
ENGINE=InnoDB;insert into t(id, k) values(1,1),(2,2);

先说结论:事务B查到的k的值是3,而事务A查到的k的值是1;

接下来,根据上面的视图数组和高低水位,来分析下事务A的语句返回的结果为什么是k=1;

这里,我们不妨做如下假设:
(1)事务A开始前,系统里面只有一个活跃事务ID是99;
(2)事务A、B、C的版本号分别是100、101、102,且当前系统里只有这四个事务;
(3)三个事务开始前,(1,1) 这一行数据的row trx_id是90;

注意一点:事务B虽然比C先开启一致性视图,但是B比C更晚执行"当前读",是否生成的trx_id应该比C更大呢?感觉这里作者应该是为了简化说明,尽量不引入后面的知识;

这样,事务A的视图数组就是[99,100],事务B的视图数组是[99,100,101],事务C的视图数组是[99,100,101,102];为了简化分析,先把其他干扰语句去掉,只画出跟事务A查询逻辑有关的操作:

从图中可以看到:

(1)第一个有效更新是事务C,把数据从(1,1)改成了(1,2);这时候,这个数据的最新版本的row trx_id是102,而90这个版本已经成为了历史版本;
(2)第二个有效更新是事务B,把数据从(1,2)改成了(1,3);这时候,这个数据的最新版本(即row trx_id)是101,而102又成为了历史版本;
(3)在事务A查询的时候,其实事务B还没有提交,但是它生成的(1,3)这个版本已经变成当前版本了;但这个版本对事务A必须是不可见的,否则就变成脏读了;

现在事务A要来读数据了,它的视图数组是[99,100];事务A查询语句的读数据流程是这样的:

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

这样执行下来,虽然期间这一行数据被修改过,但是事务A不论在什么时候查询,看到这行数据的结果都是一致的,所以我们称之为一致性读;

总结一下,对于一致性读中判断数据的可见性,规则可以简化为:一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:若版本未提交,不可见;若版本已提交,但是是在视图创建后提交的,不可见;版本已提交,而且是在视图创建前提交的,可见;

当前读

接下来看看示例中的更新逻辑,可能有疑问:事务B的update语句,如果按照一致性读,好像结果不对?因为图中事务B的视图数组是先生成的,之后事务C才提交,不是应该看不见(1,2)吗,怎么能算出(1,3)来?

先说结论:如果事务B在更新之前查询一次数据,这个查询返回的k的值确实是1;但是,当它要去更新数据的时候,就不能再在历史版本上更新了,否则事务C的更新就丢失了;因此,事务B此时的set k=k+1是在(1,2)的基础上进行的操作;

所以,这里就用到了这样一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(currentread);因此,在更新的时候,当前读拿到的数据是(1,2),更新后生成了新版本的数据(1,3),这个新版本的row trx_id是101;所以,在执行事务B查询语句的时候,一看自己的版本号是101,最新数据的版本号也是101,是自己的更新,可以直接使用,所以更新后再查询得到的k的值是3;

这里提到了一个概念,叫作当前读;其实,除了update 之类的写操作语句外,select语句如果加锁,也是当前读;例如,如果在事务A的查询语句后加上 lock in share mode 或 for update,也都可以读到当前最新的版本号是101的数据,返回的k的值是3;下面这两个select语句,就是分别加了读锁(S锁,共享锁)和写锁(X锁,排他锁);

mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;

再往前一步,假设事务C不是马上提交的,而是变成了下面的事务 C’,会怎么样呢?

事务C’的不同是,更新后并没有马上提交,在它提交前,事务B的更新语句先发起了;前面说过了,虽然事务C’还没提交,但是(1,2)这个版本也已经生成了,并且是当前的最新版本;那么,事务B的更新语句会怎么处理呢?

这时候,我们在上一篇文章中提到的"两阶段锁协议"就要上场了——行锁是在事务C’执行更新语句时加上的,要等到事务C’结束时才释放;而事务B是当前读,必须要读最新版本,而且必须加写锁,因此就被阻塞了,必须等到事务C’释放这个锁,事务B才能继续它的当前读;

事务的可重复读的能力是怎么实现的?

  • 可重复读的核心就是一致性读(consistent read);
  • 而事务更新数据的时候,只能用当前读;
  • 如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待;

隔离级别为"读提交"时的逻辑

而读提交的逻辑和可重复读的逻辑类似:

1. 在可重复读隔离级别下,只需要在事务开始执行第一条读语句的时候创建一致性视图(或使用start transaction with consistent snapshot直接创建一致性视图),之后事务里的其他查询都共用这个一致性视图;有一个特例,当前事务能读到自己最新的更新,哪怕没提交;

2. 在读提交隔离级别下,每一个slelct语句执行前都会重新算出一个新的视图(无需使用start transaction with consistent snapshot命令显示的创建一致性视图);因此可能出现同一个事物中连续的两个select读到不一样的情况,因为可能在此之间有其他事务完成了提交;

3. 不管哪个事务隔离级别,写操作都是当前读,不会去read view;

那么,我们再看一下,在读提交隔离级别下,事务A和事务B的查询语句查到的k,分别应该是多少呢?下面是读提交隔离级别下的状态图,可以看到这两个查询语句的创建视图数组的时机发生了变化,就是图中的read view框;

这时,事务A的查询语句的视图数组是在执行这个语句的时候创建的,时序上(1,2)、(1,3)的生成时间都在创建这个视图数组的时刻之前;但是,在这个时刻:(1,3)还没提交,不可见;(1,2)提交了,可见;

所以,这时候事务A查询语句返回的是k=2;显然地,事务B查询结果k=3;

小结

  • InnoDB 的行数据有多个版本,每个数据版本有自己的 row trx_id,每个事务或者语句有自己的一致性视图;
  • 普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性;有一个特例,当前事务能读到自己最新的更新,哪怕没提交;
  • 对于可重复读,查询只承认在一致性视图创建前就已经提交完成的数据;对于读提交,查询只承认在语句执行前就已经提交完成的数据;
  • 不管哪个事务隔离级别,写操作都是当前读;当前读,总是读取已经提交完成的最新版本;

思考题

条件:用下面的表结构和初始化语句作为试验环境,事务隔离级别是可重复读;

问题:现在,我要把所有“字段 c 和 id 值相等的行”的 c 值清零,但是却发现了一个“诡异”的、改不掉的情况;请你构造出这种情况,并说明其原理。

解答

因为图中更新语句后面的影响行数为0,说明更新没有成功,没有任何数据被当前事务修改;当前事务A在第一条select后生成了一致性视图read view,而当前事务又没有更新数据版本,因此更新语句后面的select是一致性读,读的是read view;

那么,为什么更新语句未影响任何一行数据呢?因为更新语句是"当前读",说明此时表t中最新的数据版本中没有满足 id =c 条件的,因此一定是在当前事务A创建一致性视图read view后,在执行更新之前,有其他的事务B更新了表t中的数据,如 update t set c=c+1,导致最新的数据版本都不满足 "id = c"的条件;

需要注意的是,事务B对数据的更新只需要在事务A的更新语句执行之前完成,等事务B完成commot后,事务A才能拿到数据的写锁,然后执行更新;并且事务B的begin命令可能在事务A之前,也可能在事务A之后;以下是几种可能的情况示例:

下篇文章:待定

本章参考:08 | 事务到底是隔离的还是不隔离的?-极客时间

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_21952.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AspectJ in action

Discovering AOP This chapter covers ■ Understanding crosscutting concerns ■ Modularizing crosscutting concerns using AOP ■ Understanding AOP languages Reflect back on your last project, and compare it with a project you worked on a few years back. Wha…

一文带你快速鉴别CookieSession

文章目录会话跟踪技术1、相关基础概念2、Cookie2.1 Cookie的基本使用2.1.1 发送Cookie2.1.2 获取Cookie2.2 Cookie原理2.3 Cookie存活时间2.4 Cookie存储中文3、Session3.1 Session的基本使用3.2 Session原理3.3 Session的钝化和活化3.4 Session的存活时间总结会话跟踪技术 1、…

SSl证书协议作用

SSl证书协议作用 随着移动互联网时代的飞速发展,似乎每周都能看到很多关于数据泄露的新闻,而且报道还在不断涌现。在统计的100款app中,有多达91款app收集了过多的用户个人信息。 为了改善这一现象,网络空间管理局联合发布了《认定…

TRC丨艾美捷TRC 2-氨基-2-甲基丙酰胺说明书

艾美捷TRC 2-氨基-2-甲基丙酰胺化学性质: 目录号A010210 化学名称2-氨基-2-甲基丙酰胺 CAS 编号16252-90-7 分子式C₄H₁₀N2O 外貌白色固体 熔点>250C(分解) 分子量102.14 溶解度甲醇(少量) 类别建筑模块…

听说2022金九银十变成铜九铁十了......

往年的金九银十,今年被戏称为“铜九铁十”。知名的大厂HR们都在不断的裁员,能被保住不被裁掉可能就万事大吉了,赛道越来越窄,都在预测未来计算机行业是不是下一个土木工程? 我也算是软件测试岗位的老鸟了,…

计算机网络03之可靠传输

1. 停止等待协议 1.概述 发送方每次只能发送一个数据包,确认方每次只能发送一个确认。发送方收到重复的确认会丢弃(接收方已经接收),接收方收到重复的数据,会把数据丢弃,但是会发送确认(防止上…

DETR:End-to-End Object Detection with Transformers

论文地址:https://arxiv.org/abs/2005.12872 代码地址:https://github.com/facebookresearch/detr 在看完Transformer之后,将会开始看视觉类的Transformer应用。本篇论文出自ECCV20,是关于目标检测的论文。DETR,即Det…

pyinstaller打包多个python程序

以下两个python文件 get_file_message_main.py为执行文件,继承了get_file_message.py中的类 打开终端cmd 切换到桌面 cd desktop切换到指定路径 cd python打包pyi-makespec pyi-makespec get_file_message_main.py生成get_file_message_main.spec文件 .spec文…

信息增益计算和决策树生长过程

信息增益计算和决策树生长过程 给定训练集S,下面以信息增益作为最佳划分的标准,演示信息增益的计算和决策树生长的过程: 根节点 (1)以“Outlook”被选做划分属性 总共有14条数据,打球9条,不打…

[Windows内核源码分析5] 引导过程(对象管理器初始化在Phase1部分的分析)

在第1阶段, ObInitSystem首先对每个处理器的PRCB结构的lookaside链表进行初始化。 在全局名字空间中创建根目录\ 来看一下NtCreateDirectoryObject这个函数实际上是ObCreateObject和ObInsertDirectory的封装。其内部执行的操作很简单,一个是创建一个目录对象, 接着…

y160.第九章 GitOps从入门到精通 -- Tekton Trigger(九)

8.Tekton Trigger 8.1 Tekton Trigger 基础 Tekton Triggers简介 监控特定的事件,并在满足条件时自动触发Tekton Pipeline; 例如,代码仓库上的创建pull request、push代码,以及合并pull request至main分支等Tekton Triggers为用户提供了一种声明式API 它允许用户按需定义监…

客户管理系统(SSM版):解除线索关联市场活动

一、客户需求: 用户在线索明细页面,点击某一个"解除关联"按钮,弹出确认解除的窗口; 用户点击"确定"按钮,完成解除线索关联市场活动的功能. *解除成功之后,刷新已经关联的市场活动列表 *解除失败,提示信息,列表也不刷新 二、功能实现 1.首…

openjdk源码准备编译和依赖

在Windows系统上进行openjdk的源码编译 一、准备编译需要的装备 1.首先下载一个软件Cygwin。这个软件是一个在Windows平台下模拟Linux运行环境的软件,提供了一系列的Linux的运行命令。(解释这些,有兴趣的自己百度) 下载的路径点…

Web APIs:事件基础

事件三要素 1.事件是有三部分组成 事件源 事件类型 事件处理程序 (1)事件源 事件被触发的对象 谁 按钮 (2)事件类型 如何触发 什么事件 比如鼠标点击(click),经过 还是键盘按下 &…

TRC丨艾美捷TRC D-Abequose说明书

艾美捷TRC D-Abequose是一种甜味剂和增味剂配方,适用于食品、饮料、药物和化妆品用途。 艾美捷TRC D-Abequose化学性质: 目录号A010205 化学名称D-Abequose CAS 编号56816-60-5 分子式C₆H₁₂O₄ 分子量148.16 贮存4C 溶解度甲醇(少许…

【蓝桥杯国赛】H 机房

蓝桥杯2022年第十三届决赛真题-机房 - C语言网 (dotcpp.com) 题意: 一共有n个结点,n-1条边,因此这是棵树 信息经过一个结点,就会产生一定的延迟,具体延迟的时间等于该结点的度数 每次询问树上两个结点,问…

c++学习

C学习Static变量生存期和作用域静态局部变量类的继承多态虚函数纯虚函数(接口)可见性数组字符串constmutable成员初始化列表三元操作符在堆、栈上创建C实例化对象C运算符和其重载thisC对象的生存期智能指针uniqueptr(作用域指针)s…

Ubuntu安装微信

1.安装wine sudo dpkg --add-architecture i386 sudo mkdir -pm755 /etc/apt/keyrings sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key//根据你的系统执行不同的命令 Ubuntu 22.04 sudo wget -NP /etc/apt/sources.list.d/…

快乐刷课---Tampermonkey下载使用

TampermonkeyChrome插件伴侣下载资源: 链接:https://pan.baidu.com/s/1IIzB8N2iPW2RjUO2pqDVHw?pwd6666 提取码:6666 1. 下载 Tampermonkey 进入油猴的官网Tampermonkey,下载你使用的浏览器对应的版本 以谷歌浏览器为例&am…

如何计算维吉尼亚密码?Java实现维吉尼亚密码的加密解密算法

文章目录如何计算维吉尼亚密码?Java实现加密算法Java实现解密算法参考博客如何计算维吉尼亚密码? 计算维吉尼亚密码有2种方式,一种是根据密码表查找,另一种是手动计算方法。 1.密码表查找法 第一行是密钥,第一列是明文…