还有一点就是 MySQL 有一个参数设置值 autocommit,默认是 1 表示的是事务自动提交,每一个查询都是一个单独的事务自动提交,就像图中事务 C,update 就是一个单独的事务,更新完自己提交。当然你可以使用显式的 begin/commit。
让我们把目光对准上面的图,事务 B 的查询结果 K 是 3,事务 A 的查询结果 K 是 1,你是不是想骂我?你先别急着骂,让我们来看下结果。打开三个控制台。
好吧我没瞎说吧,现在你可以开始骂我了,你不是说在可重复读隔离的情况下,当前事务执行过程中看到的视图始终是启动时的视图嘛。
在 MySQL 中有两个视图概念:
一个是 view。也就是创建视图,语句是 create view xxx () as....., 它并不是一个真实的表,它的内容是由存储在数据库中进行查询操作的 SQL 语句定义的。
另一个就是 InnoDB 在实现 MVCC 是用到的一致性读视图 (consistent read view)。用于支持读已提交 RC (Read Committed) 和可重复读 RR (Repeatable Read) 的隔离级别实现。
InnoDB 每一个事物都存在一个事务唯一 ID,叫做 transaction id, 它是一个事务在启动的时候 InnoDB 向系统申请的,严格按照递增的形式生成。
每一行数据都有对应多个版本,每次通过事务更新的数据都会生成新的版本,然后把 transaction id 赋值给这个版本事务 ID,记为 row trx_id, 同时,为了之后可以恢复数据,我们需要保留旧的数据版本。也就是说在当前最新的版本中,我们可以随时获取旧的数据。下图对应修改一行数据的版本图。
注:图片来源极客时间
从上图可以知道,最新版本是 V4, 且是由事务 transaction id =25 更新的,所以对应此行数据的数据版本 row trx_id =25。
按照可重复读的定义。一个事务启动的时候,可以看到所有已经提交的事务,但是接下来的事务对它来说是不可见的。所以对于一个事务启动的时候,如果一个事务在我启动时刻之前生成的,我就认,如果在我启动之后生成的,我就不认,我必须要找到它的上一个版本。有点渣男的嫌疑。如果上一个版本还不可见,那就继续往前面找,当然在这个过程中,自己更新的东西得认。
在实现上,InnoDB 为每一个事务创建了一个数组。事务中 ID 最小的称为低水位,当前系统中已经创建的事务 ID 最大值加 1 就是高水位。这个数组和水位图,就组成了当前事务的一致性图。数据版本可见性,就是基于数据版本数组和水位视图的比较而得到的结果。
这个视图数组把所有的 row trx_id 分为以下几种情况
注:图片来源极客时间
如果当前事务启动的时候,一个数据版本 (row trx_id) 落在绿色部分,表示这个事务是已提交的版本或者是自己生成的,可见。
如果落在红色部分,说明这个版本是由将来启动的事务生成的,肯定不可见。
如果落在黄色,又分为两种情况。如果 row trx_id
存在数组中,说明, 此时版本数据还未提交事务,不可见,如果不在,说明已经提交事务了,可见。
接下来可以分析为什么上面 A 查询的 k=1,B 查询的 k=3 了。
查看上图,我们假设当前有一个活跃的事务 99,目前我们更新的这一行数据的数据版本 row trx_id=90,此时系统存在 4 个事务,那么对于 A 的视图数组就是 [99,100],B 的视图数组就是 [99,100,101],C 的活跃数组就是 [99,100,101,102]。当前版本 101。
从图中知道,事务 A 先启动,接着事务 B,最后事务 C,但是第一个有效更新的是事务 C, 设置 k 为 2 (k=1+1), 然后是事务 B 把 k 设置为 3 (k=2+1), 接着到 A 查询了,他的视图数组是 [99,100], 此时数据版本 (1,3) 也就是 row trx_id=101 , 一对比,发现这货在高水位。不可见,再往回追,(1,2) 数据版本 row trx_id=102, 我去还是在高水位,不可见,最后追到 (1,1) 数据版本 row trx_id=90,比水位低,可见,一看上面的值 K=1, 所以查询结果就等于 1。
这样一个流程走下来,虽然中间修改了数据,数据版本也发生了变化,但是对于事务 A 来说,对他都是不可见的,所以看到的结果还是之前的数据。
到这里还有一个疑问,也就是开头的,事务 B 是在事务 C 之前开启事务的啊,对于事务 B 来说,事务 C 的操作对他来说是不可见的啊,事务 B 为什么获取的值是 2,然后再 2 的基础上更新了,它启动事务的时候 k 可不等于 2。
是的道理是没错,前提是如果事务 B 在更新之前先查询一遍数据,那么之后在它更新完数据以后,再次查询得到的值将会是 1, 而不是 3。这里运用到一条规则,更新数据的时候都是先读后写的。而这个读,只能读当前的值,叫做 "当前读"。对于 B 来说,在它更新之前,并没有执行读操作,所以在更新的时候,不能再在历史版本上直接更新了,否则 C 的更新将会丢失。所以 B 在更新的时候当前读 (1,2),更新之后 (1,3),当前的版本也就是 row trx_id=101 了。等到他查询的时候,一看当前版本号 101 就是自己,所以查询的时候就等于 3。
下面可以试验一下当 B 在更新之前查询一遍数据,然后更新数据再查询,符合上面所说的,得到的值就是 1。此时运用的就是当前读。
最后如果改动一下 C 事务中的执行,结果又是什么?\
这时候的事务 C 不再是自动提交事务,也是显式提交。事务 C' 更新语句,先获取写锁,但是 C' 事务还未提交,此时事务 B 是当前读,必须读取当前最新版本,而且必须加锁,等到 C 提交了,事务 B 才得以继续进行。那么 B 查询的结果不会变依然是之前的 3,而事务 C 的事务已经提交了,在读隔离的情况下,是在创建新视图之前,所以 A 的结果 k=2。
可重复读的核心就是一致性读。而事务更新数据的时候,只能用当前读,如果当前要读的记录的行数被其他事务占用的时候,就需要等待。
而读提交的逻辑和可重复读的逻辑类似,最主要的区别在于:
在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图