第七讲:事件到底是隔离的还是不隔离的?

打印 上一主题 下一主题

主题 1875|帖子 1875|积分 5625

目录

第七讲:事件到底是隔离的还是不隔离的?


媒介:

​        我在第 3 篇文章和你讲事件隔离级别的时间提到过,假如是可重复读隔离级别,事件 T 启动的时间会创建一个视图 read-view,之后事件 T 实行期间,纵然有其他事件修改了数据,事件 T 看到的仍然跟在启动时看到的一样。也就是说,一个在可重复读隔离级别下实行的事件,似乎与世无争,不受外界影响。
​        但是,我在上一篇文章中,和你分享行锁的时间又提到,一个事件要更新一行,假如刚好有别的一个事件拥有这一行的行锁,它又不能这么超然了,会被锁住,进入等待状态。问题是,既然进入了等待状态,那么比及这个事件自己获取到行锁要更新数据的时间,它读到的值又是什么呢?
示例:

​        我给你举一个例子吧。下面是一个只有两行的表的初始化语句。
  1. mysql> CREATE TABLE `t` (
  2.   `id` int(11) NOT NULL,
  3.   `k` int(11) DEFAULT NULL,
  4.   PRIMARY KEY (`id`)
  5. ) ENGINE=InnoDB;
  6. insert into t(id, k) values(1,1),(2,2);
复制代码

​        这里,我们需要留意的是事件的启动时机。
​        begin/start transaction 命令并不是一个事件的起点,在实行到它们之后的第一个操作 InnoDB 表的语句,事件才真正启动。假如你想要马上启动一个事件,可以使用 start transaction with consistent snapshot 这个命令。

  • 第一种启动方式,一致性视图是在实行第一个快照读语句时创建的;
  • 第二种启动方式,一致性视图是在实行 start transaction with consistent snapshot 时创建的。
​        还需要留意的是,在整个专栏里面,我们的例子中假如没有特别说明,都是默认 autocommit=1.
(批注:autocommit为开启状态时,纵然没有手动start transaction开启事件,mysql默认也会将用户的操作当做事件即时提交。)
​        在这个例子中,事件 C 没有显式地使用 begin/commit,表示这个 update 语句自己就是一个事件,语句完成的时间会自动提交。事件 B 在更新了行之后查询 ; 事件 A 在一个只读事件中查询,并且时间顺序上是在事件 B 的查询之后。
结果:

​        这时,假如我告诉你事件 B 查到的 k 的值是 3,而事件 A 查到的 k 的值是 1,你是不是感觉有点晕呢?
​        所以,今天这篇文章,我实在就是想和你说明白这个问题,盼望借由把这个迷惑解开的过程,可以或许帮助你对 InnoDB 的事件和锁有更进一步的明白。
视图概念

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

  • 一个是 view。它是一个用查询语句定义的虚拟表,在调用的时间实行查询语句并生成结果。创建视图的语法是 create view … ,而它的查询方法与表一样。
  • 另一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。
(批注:前者是当前读,后者是快照读。一个是在实行第一句查询语句时才会创建视图,一个是事件启动时就创建视图)
​        它没有物理结构,作用是事件实行期间用来定义“我能看到什么数据”。
深入相识MVCC

​        在第 3 篇文章《事件隔离:为什么你改了我还看不见?》中,我跟你表明过一遍 MVCC 的实现逻辑。今天为了说明查询和更新的区别,我换一个方式来说明,把 read view 拆开。你可以团结这两篇文章的说明来更深一步地明白 MVCC。
“快照”在 MVCC 里是怎么工作的?

​        在可重复读隔离级别下,事件在启动的时间就“拍了个快照”。留意,这个快照是基于整库的
​        这时,你会说这看上去不太现实啊。假如一个库有 100G,那么我启动一个事件,MySQL 就要拷贝 100G 的数据出来,这个过程得多慢啊。可是,我平常的事件实行起来很快啊。
​        实际上,我们并不需要拷贝出这 100G 的数据。我们先来看看这个快照是怎么实现的。InnoDB 里面每个事件有一个唯一的事件 ID,叫作 transaction id。它是在事件开始的时间向 InnoDB 的事件体系申请的,是按申请顺序严格递增的。而每行数据也都是有多个版本的。
​        每次事件更新数据的时间,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事件 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,可以或许有信息可以直接拿到它。
​        也就是说,数据表中的一行记录,实在可能有多个版本 (row),每个版本有自己的 row trx_id。
​        如下图所示,就是一个记录被多个事件连续更新后的状态。

​        图中虚线框里是同一行数据的 4 个版本,当前最新版本是 V4,k 的值是 22,它是被 transaction id 为 25 的事件更新的,因此它的 row trx_id 也是 25。
回顾

​        你可能会问,前面的文章不是说,语句更新会生成 undo log(回滚日志)吗?那么,undo log 在哪呢?实际上,图 2 中的三个虚线箭头,就是 undo log;而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时间根据当前版本和 undo log 计算出来的。比如,需要 V2 的时间,就是通过 V4 依次实行 U3、U2 算出来。明白了多版本和 row trx_id 的概念后,我们再来想一下,InnoDB 是怎么定义谁人“100G”的快照的。
​        按照可重复读的定义,一个事件启动的时间,可以或许看到所有已经提交的事件结果。但是之后,这个事件实行期间,其他事件的更新对它不可见。
​        因此,一个事件只需要在启动的时间声明说,“以我启动的时刻为准,假如一个数据版本是在我启动之前生成的,就认;假如是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。
​        固然,假如“上一个版本”也不可见,那就得继续往前找。
​        还有,假如是这个事件自己更新的数据,它自己还是要认的。
实现方式

​        在实现上, InnoDB 为每个事件构造了一个数组,用来生存这个事件启动瞬间,当前正在“活跃”的所有事件 ID。“活跃”指的就是,启动了但还没提交。数组里面事件 ID 的最小值记为低水位,当前体系里面已经创建过的事件 ID 的最大值加 1 记为高水位。这个视图数组和高水位,就组成了当前事件的一致性视图(read-view)。
​        而数据版本的可见性规则,就是基于数据的 row trx_id 和这个一致性视图的对比结果得到的。
​        这个视图数组把所有的 row trx_id 分成了几种差异的情况。
​       

​        这样,对于当前事件的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:

  • 假如落在绿色部分,表示这个版本是已提交的事件大概是当前事件自己生成的,这个数据是可见的;
  • 假如落在赤色部分,表示这个版本是由将来启动的事件生成的,是肯定不可见的;
  • 假如落在黄色部分,那就包括两种情况

    • a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事件生成的,不可见;
    • b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事件生成的,可见。

​        比如,对于图 2 中的数据来说,假如有一个事件,它的低水位是 18,那么当它访问这一行数据时,就会从 V4 通过 U3 计算出 V3,所以在它看来,这一行的值是 11。
​        你看,有了这个声明后,体系里面随后发生的更新,是不是就跟这个事件看到的内容无关了呢?因为之后的更新,生成的版本一定属于上面的 2 大概 3(a) 的情况,而对它来说,这些新的数据版本是不存在的,所以这个事件的快照,就是“静态”的了。
​        所以你现在知道了,InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的本领。
​        接下来,我们继续看一下图 1 中的三个事件,分析下事件 A 的语句返回的结果,为什么是 k=1。
​        这里,我们不妨做如下假设:

  • 事件 A 开始前,体系里面只有一个活跃事件 ID 是 99;
  • 事件 A、B、C 的版本号分别是 100、101、102,且当前体系里只有这四个事件;
  • 三个事件开始前,(1,1)这一行数据的 row trx_id 是 90。
​        这样,事件 A 的视图数组就是[99,100], 事件 B 的视图数组是[99,100,101], 事件 C 的视图数组是[99,100,101,102]。
​        为了简化分析,我先把其他干扰语句去掉,只画出跟事件 A 查询逻辑有关的操作:

​        从图中可以看到,第一个有效更新是事件 C,把数据从 (1,1) 改成了 (1,2)。这时间,这个数据的最新版本的 row trx_id 是 102,而 90 这个版本已经成为了历史版本。
​        第二个有效更新是事件 B,把数据从 (1,2) 改成了 (1,3)。这时间,这个数据的最新版本(即 row trx_id)是 101,而 102 又成为了历史版本。
​        你可能留意到了,在事件 A 查询的时间,实在事件 B 还没有提交,但是它生成的 (1,3) 这个版本已经酿成当前版本了。但这个版本对事件 A 必须是不可见的,否则就酿成脏读了。
​        好,现在事件 A 要来读数据了,它的视图数组是[99,100]。固然了,读数据都是从当前版本读起的。所以,事件 A 查询语句的读数据流程是这样的:

  • 找到 (1,3) 的时间,判断出 row trx_id=101,比高水位大,处于赤色地区,不可见;
  • 接着,找到上一个历史版本,一看 row trx_id=102,比高水位大,处于赤色地区,不可见;
  • 再往前找,终于找到了(1,1),它的 row trx_id=90,比低水位小,处于绿色地区,可见。
​        这样实行下来,虽然期间这一行数据被修改过,但是事件 A 不论在什么时间查询,看到这行数据的结果都是一致的,所以我们称之为一致性读。
​        这个判断规则是从代码逻辑直接转译过来的,但是正如你所见,用于人肉分析可见性很麻烦。
​        所以,我来给你翻译一下。一个数据版本,对于一个事件视图来说,除了自己的更新总是可见以外,有三种情况:

  • 版本未提交,不可见;
  • 版本已提交,但是是在视图创建后提交的,不可见;
  • 版本已提交,而且是在视图创建前提交的,可见。
​        现在,我们用这个规则来判断图 4 中的查询结果,事件 A 的查询语句的视图数组是在事件 A 启动的时间生成的,这时间:

  • (1,3) 还没提交,属于情况 1,不可见;
  • (1,2) 虽然提交了,但是是在视图数组创建之后提交的,属于情况 2,不可见;
  • (1,1) 是在视图数组创建之前提交的,可见。
​        你看,去掉数字对比后,只用时间先后顺序来判断,分析起来是不是轻松多了。所以,后面我们就都用这个规则来分析。
更新逻辑

​        细心的同学可能有疑问了:事件 B 的 update 语句,假如按照一致性读,似乎结果不对哦?
​        你看图中,事件 B 的视图数组是老师成的,之后事件 C 才提交,不是应该看不见 (1,2) 吗,怎么能算出 (1,3) 来?

​        是的,假如事件 B 在更新之前查询一次数据,这个查询返回的 k 的值确实是 1。但是,当它要去更新数据的时间,就不能再在历史版本上更新了,否则事件 C 的更新就丢失了。因此,事件 B 此时的 set k=k+1 是在(1,2)的基础上进行的操作。所以,这里就用到了这样一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。
​        因此,在更新的时间,当前读拿到的数据是 (1,2),更新后生成了新版本的数据 (1,3),这个新版本的 row trx_id 是 101。
​        所以,在实行事件 B 查询语句的时间,一看自己的版本号是 101,最新数据的版本号也是 101,是自己的更新,可以直接使用,所以查询得到的 k 的值是 3。
​        这里我们提到了一个概念,叫作当前读。实在,除了 update 语句外,select 语句假如加锁,也是当前读。
​        所以,假如把事件 A 的查询语句 select * from t where id=1 修改一下,加上 lock in share mode 或 for update,也都可以读到版本号是 101 的数据,返回的 k 的值是 3。下面这两个 select 语句,就是分别加了读锁(S 锁,共享锁)和写锁(X 锁,排他锁)。
  1. mysql> select k from t where id=1 lock in share mode;
  2. mysql> select k from t where id=1 for update;
复制代码
​        再往前一步,假设事件 C 不是马上提交的,而是酿成了下面的事件 C’,会怎么样呢?

​        事件 C’的差异是,更新后并没有马上提交,在它提交前,事件 B 的更新语句先发起了。前面说过了,虽然事件 C’还没提交,但是 (1,2) 这个版本也已经生成了,并且是当前的最新版本。那么,事件 B 的更新语句会怎么处置惩罚呢?这时间,我们在上一篇文章中提到的“两阶段锁协议”就要上场了。事件 C’没提交,也就是说 (1,2) 这个版本上的写锁还没释放。而事件 B 是当前读,必须要读最新版本,而且必须加锁,因此就被锁住了,必须比及事件 C’释放这个锁,才气继续它的当前读。

​        到这里,我们把一致性读、当前读和行锁就串起来了。
​        现在,我们再回到文章开头的问题:事件的可重复读的本领是怎么实现的?可重复读的核心就是一致性读(consistent read);而事件更新数据的时间,只能用当前读。假如当前的记录的行锁被其他事件占用的话,就需要进入锁等待。而读提交的逻辑和可重复读的逻辑类似,它们最重要的区别是:

  • 在可重复读隔离级别下,只需要在事件开始的时间创建一致性视图,之后事件里的其他查询都共用这个一致性视图;
  • 在读提交隔离级别下,每一个语句实行前都会重新算出一个新的视图。
​        那么,我们再看一下,在读提交隔离级别下,事件 A 和事件 B 的查询语句查到的 k,分别应该是多少呢?
​        这里需要说明一下,“start transaction with consistent snapshot; ”的意思是从这个语句开始,创建一个连续整个事件的一致性快照。所以,在读提交隔离级别下,这个用法就没意义了,等效于平常的 start transaction。
​        下面是读提交时的状态图,可以看到这两个查询语句的创建视图数组的时机发生了变化,就是图中的 read view 框。(留意:这里,我们用的还是事件 C 的逻辑直接提交,而不是事件 C’)

​        这时,事件 A 的查询语句的视图数组是在实行这个语句的时间创建的,时序上 (1,2)、(1,3) 的生成时间都在创建这个视图数组的时刻之前。但是,在这个时刻:
​        (1,3) 还没提交,属于情况 1,不可见;
​        (1,2) 提交了,属于情况 3,可见。
​        所以,这时间事件 A 查询语句返回的是 k=2。显然地,事件 B 查询结果 k=3。
小结

​        InnoDB 的行数据有多个版本,每个数据版本有自己的 row trx_id,每个事件大概语句有自己的一致性视图。平常查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定命据版本的可见性。

  • 对于可重复读,查询只承认在事件启动前就已经提交完成的数据;
  • 对于读提交,查询只承认在语句启动前就已经提交完成的数据;
  • 而当前读,总是读取已经提交完成的最新版本。
​        你也可以想一下,为什么表结构不支持“可重复读”?这是因为表结构没有对应的行数据,也没有 row trx_id,因此只能遵照当前读的逻辑。
​        固然,MySQL 8.0 已经可以把表结构放在 InnoDB 字典里了,也许以后会支持表结构的可重复读。
深入:

NO.1

启动视图后,后面的事件变动是看不到的。 但是变动后的版本是可以或许看到的。 比如一行记录有1->2->3->4->5,5个版本。 但是我们这个事件视图A是在这个3瞬间启动的。 那视图A是怎么拿到3这个值呢?记住它不是直接在视图中生存的3这个值,而是通过视图版本末了的一条数据,通过undo log 然后一个个从后往前找,先找到5 ,然后这个row tra id 不属于这个视图中,丢弃。 继续找4,不属于。通过4 找到3 。row tra id 属于这个事件视图中,则该视图中认为3是这行的值。
思索:

​        又到思索题时间了。我用下面的表结构和初始化语句作为试验情况,事件隔离级别是可重复读。现在,我要把所有“字段 c 和 id 值相等的行”的 c 值清零,但是却发现了一个“诡异”的、改不掉的情况。请你构造出这种情况,并说明其原理。
  1. mysql> CREATE TABLE `t` (
  2.   `id` int(11) NOT NULL,
  3.   `c` int(11) DEFAULT NULL,
  4.   PRIMARY KEY (`id`)
  5. ) ENGINE=InnoDB;
  6. insert into t(id, c) values(1,1),(2,2),(3,3),(4,4);
复制代码

​        复现出来以后,请你再思索一下,在实际的业务开发中有没有可能碰到这种情况?你的应用代码会不会掉进这个“坑”里,你又是怎么解决的呢?
答案:


​        这样,session A 看到的就是我截图的结果了。实在,还有别的一种场景,同学们在留言区都还没有提到。

​        这个操作序列跑出来,session A 看的内容也是可以或许复现我截图的结果的。这个 session B’启动的事件比 A 要早,实在是上期我们描述事件版本的可见性规则时留的彩蛋,因为规则里还有一个“活跃事件的判断”,我是准备留到这里再补充的。
​        当我试图在这里讲述完备规则的时间,发现第 8 篇文章《事件到底是隔离的还是不隔离的?》中的表明引入了太多的概念,以致于分析起来非常复杂
​        用新的方式来分析 session B’的更新为什么对 session A 不可见就是:在 session A 视图数组创建的瞬间,session B’是活跃的,属于“版本未提交,不可见”这种情况。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

麻花痒

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表