可重复读隔离级别
在可重复读隔离级别的事务A的执行过程中,看到的数据都是一致的,即使是其他事务B修改了数据,在事务A中看到的数据都是一摸一样的,和启动事务A开始的数据保持一致。
这里指的数据一致并不是说是事务A启动之前看到的数据是1000,然后在事务A的执行过程中看到的这个数据一直就是1000,而是说在事务A启动开始的那一刻到提交事务的那一刻,整个事务A的执行过程中看到的这个数据的值都是一样的,可能一直就是1000,也可能就一直都是2000(其它事务的参与),但是数值是不会有不一致的情况。
在行锁中有提到,前事务拥有了行锁,后事务必须等待前事务提交完,才能获得行锁。在等待的过程中,前事务如果更改了数据行的值,那后事务看到的数据行的值是什么了?
begin/start transaction命令开启事务并不是就是一个事务开启的起点,而是需要等到真正执行sql的时候才是开启事务的起点;
start transaction with consistent snapshot命令就是立刻启动这个事务。
事务C中没有显式的begin/commit,但是正常情况下autocommit=1,事务C也就是执行完就提交的意思。
可重复读隔离级别事务在开始的时候,会创建一个视图,这个事务中查看到的数据都是从这个视图中看到的,从而保证事务中看到的数据来源是一致的;而这个视图中的数据相当于对事务开启的那一刻的整个库的数据拍了个快照。
但是整个快照并不是把整个数据库的数据进行快照,如果一个数据库容量有100G,那创建一个事务就需要100G的快照,那任意一个更新语句占的内存就太大了。
InnoDB中,每个事务都会有个事务id,叫作transaction id;它是在事务开始的时候向 InnoDB的事务系统申请的,是按申请顺序严格递增的。
而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且 把transaction id赋值给这个数据版本的事务ID,记为rowtrx_id。同时,旧的数据版本要保留, 并且在新的数据版本中,能够有信息可以直接拿到它。 也就是说,数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的rowtrx_id。
v1,v2,v3,v4都是一行数据的不同版本,v4是最新的版本,也就是当前版本;并且可以看到每个版本中都是保存了事务id的;其中的U1,U2,U3就是undo log,也就是之前提到过的两阶段提交中将操作内容写入到日志中,然后才写入磁盘;所以当需要v3版本的数据的时候,就是通过v4版本值再经过u3中记录的操作,反推到v3版本的值。
按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这 个事务执行期间,其他事务的更新对它不可见。因此,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我 启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版 本”。当然,如果“上一个版本”也不可见,那就得继续往前找。还有,如果是这个事务自己更新的数 据,它自己还是要认的。
在实现上,InnoDB为了每个事务都创建一个数组,数组里存放的就是当前时刻活跃的事务,活跃的事务就是值已经创建的事务,但是还未提交的事务。数组中最小的事务id的值被称为低水位,已经创建过的事务的id的最大值+1被记为高水位;因为数组存放的是当前时刻活跃的事务,并且事务的id还是严格递增的,所以可以直到数组的最大值肯定就是此刻创建的事务id的值,数组中最大的值肯定是小于高水位的。整个数组是每个事务独有的,最小值可能相同,但是最大值肯定是不同的,因为最大值是自身的值。
这样,对于当前事务的启动瞬间来说,一个数据版本的rowtrx_id,有以下几种可能:
- 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是 可见的;
- 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
- 如果落在黄色部分, rowtrx_id在数组中,表示这个rowtrx_id对对应的事务还是属于活跃的事务,还未提交的事务的版本,所以不可见;
- 如果落在黄色部分, rowtrx_id不在数组中,表示 rowtrx_id对应的那个事务已经不是活跃的事务,是已经提交的事务,所以数据可见。
做如下假设:
- 事务A开始前,系统里面只有一个活跃事务ID是99;
-
- 事务A、B、C的版本号分别是100、101、102,且当前系统里只有这四个事务;
-
- 三个事务开始前,(1,1)这一行数据的rowtrx_id是90。 这样,事务A的视图数组就是[99,100], 事务B的视图数组是[99,100,101], 事务C的视图数组是 [99,100,101,102]
在事务执行过程中,虽然还未提交这个事务,但是在事务中对这一行的版本更新是可以被其他事务看见的。
A:
- 查询当前版本发现版本的rowtrx_id是101,是大于事务A的活跃数组的最大值(即100),属于高水位以上,是未来事务,不可见。
- 经过日志存储的操作记录,计算出历史版本1的数据,看见rowtrx_id是102,还是高水位以上,不可见。
- 回退计算,得到历史版本2,发现rowtrx_id是90,是低水位以下,可见,所以A查询的值是1
在B事务中,有一个更新操作,如果事务B的更新操作是在事务B创建的视图中进行的,那就把事务c的操作给覆盖了。
**
所以,这里就用到了这样一条规则:更新数据都是先读后写的,而这个读,只能读当前的 更 值,称为 值 “当前读 当 ”(current read
)。
**
事务B中更新之前,先去读当前数据,所以更新的目标值已经是2了,所以给更新成3,此时数据版本更新的当前版本对应的rowtrx_id是101。
在事务B的查询中,发现当前版本的rowtrx_id是101,在事务B的活跃数组中,所以可见,看见的值是3。
如果事务C做一些改动:
改动就是事务C的提交是在事务B的更新操作,查询操作之后,在事务B的提交之前。但是由于之前说到的两阶段锁,新事务C的写锁还未提交,所以事务B的读锁(更新操作必须当前读)是互斥的,所以事务B只能等待锁的释放。