菜鸟笔记
提升您的技术认知

事务到底是隔离还是不隔离?-ag真人游戏

之前我们探讨过可重复读隔离级别下,事务t启动的时候会创建一个视图read-view。在事务t执行期间,即使有其他事务修改了数据,事务t看到的也是跟启动时一样的。

但是上次讲到行锁的时候,当事务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);

然后分别执行事务a、事务b、事务c

这里我们需要注意事务的启动时机,启动实际是存在两种方式的:

  1. begin/start transaction:这种命令的启动方式并不是立即启动事务,而是到执行第一个innodb的操作语句时,事务才真正启动。也就是说一致性视图是在第一个快照读语句时创建的
  2. start transaction with consistent snapshot:这种命令的启动方式能够立即启动事务。也就是说一致性视图是在执行该语句的时候就创建了

对于上图中,事务c没有显式地启动事务,但是执行完update语句就会立马提交。事务b在更新了行之后查询。事务a是一个只读地事务,且在事务b和事务c更新之后进行查询。

最终的答案是,事务a看到的值是1,事务b看到的值是3。

在mysql中视图有两个含义:

  1. view:一个用查询语句定义的虚拟表,创建视图的语法是create view…
  2. consistent read view:innodb在实现mvcc时用到的一致性读视图,用于支持read committed(读提交)和repeatable read(可重复读)隔离级别的实现。

在可重复读隔离级别下,事务在启动的时候就建立了一张快照,并且这个快照是基于整库的

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

而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新版本的数据,并且把transaction id赋值给这个数据版本的事务id,记为row trx_id。也就是说,数据表中的一行数据,其实可能有多个版本,每个版本有自己的row trx_id。

如下图所示,就展现了一个记录被多个事务更新的状态。

v1、v2、v3并不是物理上实际存在的,而是每次需要的时候根据当前版本和redo log(回滚日志)计算出来的。比如,需要v2的时候,通过v4依次执行u3、u2算出来的。

接下来,我们来看快照是如何生成的?

根据可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。
也就是说,如果一个事务是在我启动之前生成的,就认。如果是我启动之后生成的,我就不认,必须要找到上一个可见的版本。此外,如果这个事务自己更新的数据,也是要认的

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

数组里面事务id的最小值记为低水位,当前系统里面已经创建过的事务id的最大值加1记为高水位。这个视图数组和和高水位,就组成了当前事务的一致性视图

数据版本的可见性规则,就是基于数据的row trx_id和这个一致性视图对比结果得到的。如下图,分为以下几种情况:

对于当前事务的启动瞬间,一个数据版本的row trx_id,有以下几种可能:

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

接着上面的说,对于第一个图中,如果有一个事务,它的低水位是18,那么当访问到这行数据的时候,就会从v4回滚到v3,在他看来,这一行的值是11。innodb利用了“所有数据都有多个版本”实现了秒级快照的能力。

然后,我们再回到最开始的引例,分析下为什么事务a看到的值是k=1。
我们先做如下的假设:

  1. 事务a开始前,系统分配的最大transaction 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查询语句读取数据的流程是这样的

  1. 找到(1,3)的时候,发现row trx_id=101,比高水位大,处于红色区域,不可见。
  2. 找到上一个历史版本(1,2),发现row trx_id=102,比高水位大,处于红色区域,不可见。
  3. 再找到上一个历史版本(1,1),发现row trx_id=90,比低水位小,处于绿色区域,可见。

但是上面这种判断方法太麻烦,我们可以简化判断方法

  • 前提:除了自己的更新总是可见之外
  • 版本未提交,不可见
  • 版本已提交,但是在视图创建后提交,不可见
  • 版本已提交,但是在视图创建前提交,可见。

我们再基于这种方法来判断事务a的查询流程:

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

可能你会发现,如果按照上面的逻辑,事务b不应该也看不见(1,2)吗?下面是事务b的事务逻辑图:

如果事务b只更新数据的话,那看到的确实是(1,1)。但是事务b先更新了数据,那么就不能在历史版本上更新了,否则就会出现丢失更新的情况。因此事务b的set k=k 1是在(1,2的基础上)完成的。

所以这里用到了一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。 因此,在执行事务b查询语句的时候,自己的版本号和最新的数据版本号都是101,所以查询得到k的值是3。

其实除了update语句外,select语句如果加锁,也是当前读。
因此,如果把事务a的查询语句,加上lock in share mode或for update,也都是返回k等于3。如下面这两个select语句,就是分别加了读锁和写锁。

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

再分析另外一种情况,如果事务c不是马上提交会怎么样?如下图:

因为事务c先将k修改为2,根据之前的两阶段协议,由于还没有提交,所以事务c会持有该行的行锁,直到事务提交才释放。又由于事务b是当前读,必须要读到最新版本,所以就需要等待事务c释放了锁才能修改值。如下图所示:

接下来,我们总结下事务的可重复读的能力是如何实现的
可重复读的核心就是一致性读;而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进行等待。

而对于读提交的逻辑其实也很像,主要区别在于?

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

那么在读提交的隔离级别下,事务a和事务b查询到的值是多少呢?

事务b的查询结果为k=3.但是由于事务b还没提交,事务c提交了,所以事务a的查询结果为k=2。

网站地图