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

专栏前面的文章提到过如果是可重复读隔离级别,事务T启动的时候会创建一个read-view,之后事务T执行期间,即使有其他事务修改了数据,事务T看到的仍然跟在启动时看到的一样。就是说一个在可重复读隔离级别下执行的事务,好像与世无争,不受外界影响。

但是专栏上一篇文章描述行锁的时候说到一个事务要更新一行,如果刚好有另外一个事务拥有这一行的行锁,不能这么超然会被锁住,进入等待状态。

那么问题是既然进入了等待状态,等到这个事务自己获取到行锁要更新数据的时候,读到的值又是什么???

来看一个只有两行表的初始化语句:

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表的语句,事务才启动。可以使用start transaction with consistent snapshot来马上开启一个事务。

事务C没有显示使用begin/commit,说明update语句本身就是一个事务语句完成自动提交。事务B更新行之后查询;事务A在一个只读事务中查询,并且时间顺序上是在事务B查询之后。

这时, 如果我告诉你事务B查到的k的值是3, 而事务A查到的k的值是1, 你是不是感觉有点晕
呢?本篇专栏将会把这个问题说明白。

在MySQL有两个视图概念:

  • 一个是view。它是一个用查询语句定义的虚拟表,在调用时候执行查询语句并生成结果。创建视图的语法是create view…,查询方法和表一样。
  • 另一个是InnoDB在实现MVCC时用到的一致性读视图,即consistent read view, 用于支持RC( Read Committed, 读提交) 和RR( Repeatable Read, 可重复读) 隔离级别的实现。

“快照”在MVCC中是如何工作的?

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

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

数据表中的一行记录,其实可能有很多个版本(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算出来。

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

在实现上, InnoDB为每个事务构造了一个数组, 用来保存这个事务启动瞬间, 当前正在“活
跃”的所有事务ID。 “活跃”指的就是, 启动了但还没提交。

数组里面事务ID的最小值记为低水位, 当前系统里面已经创建过的事务ID的最大值加1记为高水位。

这个视图数组和高水位, 就组成了当前事务的一致性视图( read-view) 。

而数据版本的可见性规则, 就是基于数据的row trx_id和这个一致性视图的对比结果得到的。

这个视图数组把所有的row trx_id 分成了几种不同的情况:
在这里插入图片描述
对于当前事务的启动瞬间来说, 一个数据版本的row trx_id, 有以下几种可能:

  1. 如果落在绿色部分, 表示这个版本是已提交的事务或者是当前事务自己生成的, 这个数据是可见的;
  2. 如果落在红色部分, 表示这个版本是由将来启动的事务生成的, 是肯定不可见的;
  3. 如果落在黄色部分, 那就包括两种情况
    a. 若 row trx_id在数组中, 表示这个版本是由还没提交的事务生成的, 不可见;
    b. 若 row trx_id不在数组中, 表示这个版本是已经提交了的事务生成的, 可见。

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

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

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

继续看一下图1中的三个事务, 分析下事务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查询逻辑有关的操作:
在这里插入图片描述
从图中可以看到, 第一个有效更新是事务C, 把数据从(1,1)改成了(1,2)。 这时候, 这个数据的最新版本的row trx_id是102, 而90这个版本已经成为了历史版本。

第二个有效更新是事务B, 把数据从(1,2)改成了(1,3)。 这时候, 这个数据的最新版本( 即rowtrx_id) 是101, 而102又成为了历史版本。

在事务A查询的时候, 其实事务B还没有提交, 但是它生成的(1,3)这个版本已经变成当前版本了。 但这个版本对事务A必须是不可见的, 否则就变成脏读了。

事务A查询语句的读数据流程是这样的:

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

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

  1. 版本未提交, 不可见;
  2. 版本已提交, 但是是在视图创建后提交的, 不可见;
  3. 版本已提交, 而且是在视图创建前提交的, 可见

现在, 我们用这个规则来判断图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) 。

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

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

读提交的逻辑和可重复读的逻辑类似, 它们最主要的区别是:

  • 在可重复读隔离级别下, 只需要在事务开始的时候创建一致性视图, 之后事务里的其他查询都共用这个一致性视图;
  • 在读提交隔离级别下, 每一个语句执行前都会重新算出一个新的视图

下面是读提交时的状态图, 可以看到这两个查询语句的创建视图数组的时机发生了变化, 就是图中的read view框。 ( 注意: 这里, 我们用的还是事务C的逻辑直接提交, 而不是事务C’)
在这里插入图片描述
事务A的查询语句的视图数组是在执行这个语句的时候创建的, 时序上(1,2)、 (1,3)的生成
时间都在创建这个视图数组的时刻之前。 但是, 在这个时刻:

  • (1,3)还没提交,属于情况1不可见
  • (1,2)提交属于情况3,可见

所以事务A查询语句返回的是k=2
显然事务B查询结果是k=3

总结

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

而当前读, 总是读取已经提交完成的最新版本。

表结构不支持“可重复读”? 这是因为表结构没有对应的行数据, 也没有row trx_id, 因此只能遵循当前读的逻辑。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值