以下面一个表举例

mysql如何实现可重复读 mysql怎么实现可重复读_mysql如何实现可重复读

A,B,C三个事务,执行的顺序如下,这默认autocommit = 1:

mysql如何实现可重复读 mysql怎么实现可重复读_mysql如何实现可重复读_02

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

      先说明执行的结果,事务B查到的k值是3,事务A查到的k值是1.

      在MySQL中,视图有两种概念:

  1. view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是create view … ,而它的查询方法与表一样。
  2. InnoDB在多版本並行控制(MVCC)中使用的一致性视图,即consistent read view,用来支持读提交(RC, Read Commit), 可重复读(Repeatable Read)隔离级别的实现。

快照在MVCC中的工作机制

              在可重复读的隔离级别下,事务在启动的时候,会生成一个快照,这个快照并不是把整个数据库都复制下来。

              在InnoDB中,每个事务都有一个事务ID,他是在事务开始的时候向InnoDB申请的,按照顺序严格递增。每行数据是有多个版本的,每次事务更新数据的时候,都会生成一个新的数据版本,并且把事务ID赋值给这个数据版本的事务ID,记作rowtrx_id。同时,旧的数据版本也要保留,那么,数据表中的一行记录,其实可能有多个版本(row),每个版本都有自己的rowtrx_id。如下图,就是一个记录被多个事务连续更新后的状态。

mysql如何实现可重复读 mysql怎么实现可重复读_数组_03

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

其中三个虚线的箭头,就是undo log(回滚日志),其实V1,V2,V3不是物理存在的,而是每次需要的时候,就要根据undo log一个个计算出来。

这么看来,一个事务启动的时候,就能看到所有已经提交的事务结果,但是之后这个事务执行的期间,其他的事务的更新对他不可见。即在启动时,一个数据的版本是在启动之前生成的,那可见,如果是启动之后生成的,那么不可见,就网上找一个版本,如果上个版本也不可见,就继续找。当然如果这个数据是自己更新的数据,也停止查找。

可以理解为InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前启动了但还没提交的所有事务ID。数组里面事务ID最新的值记为低水位,事务ID最大值加1为高水位,数组和高水位,组成了当前事务的一致性视图(read-view)。数据版本的可见性规则,就是基于数据的row trx_id和这个一致性视图的对比结果得到的。

mysql如何实现可重复读 mysql怎么实现可重复读_mysql如何实现可重复读_04

row trx_id 有几种不同的情况:

  1. 落在绿色部分,说明这个版本是已提交的事务或者是当前事务自己生成的,说明数据是可见。
  2. 如果落在红色部分,说明是为启动的事务的,不可见。
  3. 落在黄色部部分,就分两种

    a. 若 row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见;

    b. 若 row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。

如果,有一个事务,它的低水位是18,那么当它方位这一行数据时,就会从V4通过U3计算出V3,所以在他看来这个值一直是11。

查询逻辑

回到文章刚刚开始时的例子,看看为啥A中查询到的k为1.

假设:

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

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

mysql如何实现可重复读 mysql怎么实现可重复读_隔离级别_05

上面的流程如下:

  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],它读数据的时候是这样子的流程:

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

这样,A的读取满足可重复读的原则。

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

       1. 版本未提交,不可见;

2. 版本已提交,但是是在视图创建后提交的,不可见;

3. 版本已提交,而且是在视图创建前提交的,可见。

这时候我们再看看A的读取过程:

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

 

更新逻辑

来看看文章开头例子的事务B的更新和读取。

当B去更新数据的时候,此时事务C已经完成更新了,那么他就不能在历史版本上更新了,否则事务C就丢失了,因此,事务B在执行的时候,是在(1,2)基础上进行的。更新数据都是先读后写的,而这个读,只能读当前的值,而不是事务启动的时候的读,跟可重复读不一样,称为“当前读”(current read)。

当前读拿到的数据是(1,2),更新后生成了新版本的数据(1,3),这个新版本的row trx_id是101。所以,在执行事务B查询语句的时候,一看自己的版本号是101,最新数据的版本号也是101,是自己的更新,可以直接使用,所以查询得到的k的值是3。

Ps. 除了update语句外,select语句如果加锁,也是当前读,语句如下:
select k from t where id=1 lock in share mode; 读锁(S锁,共享锁)
select k from t where id=1 for update; 写锁(X锁,排他锁)。

再看一下,如果事务C不是马上提交的,而是变成了以下这样:

这个时候,就要遵守两阶段锁协议了,事务C还没有提交,而事务B是“当前读”,那么就一定要加锁,要等到事务C释放这个锁,B才能往下。

读提交下的场景:

mysql如何实现可重复读 mysql怎么实现可重复读_mysql如何实现可重复读_06

事务A的查询语句的视图数组是在执行这个语句的时候创建的,时序上(1,2)、(1,3)的生成时间都在创建这个视图数组的时刻之前。

mysql如何实现可重复读 mysql怎么实现可重复读_mysql如何实现可重复读_07

但是,在这个时刻:

  1.  (1,3)还没提交,属于情况1,不可见;
  2.  (1,2)提交了,属于情况3,可见。

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

总结:

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

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

  1.   在可重复读隔离级别下,查询只承认在事务启动前就已经提交完成的数据。只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
  2.   在读提交隔离级别下,查询只承认在语句启动前就已经提交完成的数据。每一个语句执行前都会重新算出一个新的视图。所以说,在读提交隔离级别下,start transaction with consistent snapshot这个用法就没意义了,等效于普通的start transaction。