前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MySQL基础篇7 mysql的事务到底是隔离的还是不隔离的

MySQL基础篇7 mysql的事务到底是隔离的还是不隔离的

原创
作者头像
历久尝新
修改2020-05-20 17:28:10
6900
修改2020-05-20 17:28:10
举报
文章被收录于专栏:学而时习之学而时习之

正文: 弄清楚事务中的事务隔离与实现

如果是可重复读级别. 事务T启动的时候会创建一个视图read-view. 之后事务T之星期间, 即使有其他事务修改了数据, 事务T看到的仍然跟在启动时候看到的一样.

也就是说, 在可重复读隔离级别下执行事务, 好像与世无争. 不受外界影响.

但是在学习行锁的时候, 又提到了, 一个事务要更新一行, if 刚好有另外一个事务拥有这一行的行锁, 它又不不能这么超然了, 会被锁住. 进入等待状态. 问题是, 既然进入了等待状态, 那么等到这个事务自己获取到行锁要更新数据的时候, 他读到又是什么呢?

举个栗子: 下面是一个只有两行的表的初始化语句.

代码语言:javascript
复制
mysql> 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);

这里我们要注意的是事务的启动时机.

begin/start transaction 命令并不是一个事务的起点, 在执行到他们之后的第一个操作innodb表的语句, 才是真正的启动.

如果想立刻启动一个一个事务可以使用transaction with consistent snapshot 这个命令.

  • 第一种启动方式. 一致性视图是在执行第一个快照读语句时创建的;
  • 第二种启动方式, 一致性视图是在执行transaction with consistent snapshot 时创建的.

一般默认的autocommit = 1;

在这个例子中,

事务c没有没有显式的使用begin/commit, 表示update 这个语句本身就是一个事务. 语句完成的时候会自动提交.

事务B在更新了行之后查询

事务A在一个只读事务中查询

结果: B的k值是3, A 的K值是1,

解惑

在mysql里, 有两个"视图"的概念

  1. 一个是view. 他是一个用查询语句定义的虚拟表. 在调用的时候执行查询语句并生成结果. 创建视图的语法是create view ..., 而他的查询方法与表一样.
  2. 另一个是innodb在实现mvcc时用到的一致性读视图. 即consistent read view. 用于支持rc和rr隔离级别的实现.

他没有物理结构, 作用是事务执行期间用来定义. "我能看到什么数据"

"快照"在mvcc里是如何工作的?

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

快照是如何实现的?

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

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

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

行状态变更图
行状态变更图

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

那么undo log 在哪?

事实上. 上图的三个虚线剪头就是undo log.

而v1. v2. v3. 并不是物理上真实存在的. 而是每次需要的时候根据当前的版本和undo log 计算出来的.

比如. 需要v2的时候, 就是通过V4 依次的执行U3 U2算出来的.

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 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

比如,对于上上图的数据来说,如果有一个事务,它的低水位是 18,那么当它访问这一行数据时,就会从 V4 通过 U3 计算出 V3,所以在它看来,这一行的值是 11。

有了这个声明后,系统里面随后发生的更新,是不是就跟这个事务看到的内容无关了呢?因为之后的更新,生成的版本一定属于上面的 2 或者 3(a) 的情况,而对它来说,这些新的数据版本是不存在的,所以这个事务的快照,就是“静态”的了.

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

我们继续看一下上上上图中的三个事务. 分析下事务A的语句返回的结果. 为什么返回的k=1

做一个假设:

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

这样,事务 A 的视图数组就是[99,100], 事务 B 的视图数组是[99,100,101], 事务 C 的视图数组是[99,100,101,102]

为了简化分析,先把其他干扰语句去掉,只画出跟事务 A 查询逻辑有关的操作:扰语句去掉,只画出跟事务 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 不论在什么时候查询,看到这行数据的结果都是一致的,所以我们称之为一致性读。

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

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

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

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

更新逻辑

有一个问题: 事务B的update语句 如果按照一致性读, 好像结果不太对.

看下图. 事务B的视图数组是先生成的. 之后事务c才提交, 不是应该看不见1,2 怎么能算出来1, 3 呢?

事务b更新逻辑图
事务b更新逻辑图

是的,如果事务 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 锁,排他锁)

代码语言:go
复制
mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;

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

事务ABC`的执行流程
事务ABC`的执行流程

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

这时候,我们在上一篇文章中提到的“两阶段锁协议”就要上场了。事务 C’没提交,也就是说 (1,2) 这个版本上的写锁还没释放。而事务 B 是当前读,必须要读最新版本,而且必须加锁,因此就被锁住了,必须等到事务 C’释放这个锁,才能继续它的当前读

事务 B 更新逻辑图(配合事务 C')
事务 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 和一致性视图确定数据版本的可见性。

  • 对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
  • 对于读提交,查询只承认在语句启动前就已经提交完成的数据;

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 正文: 弄清楚事务中的事务隔离与实现
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档