MySQL事务隔离(2)
到底是隔离还是不隔离
假如现在有如下表:
id | k |
---|---|
1 | 1 |
2 | 2 |
现在执行如下操作:
在可重复读的隔离下,这里面事务B读取到的k的值是3,而事务A读取到的k是1。下面看具体原因。
在MySQL中,有两个视图概念:
第一个是view,这里就是查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。
另一个是InnoDB在实现MVCC时用到的一致性读视图,即consistent read view,用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现。
可重复读隔离主要用到第二个视图。
“快照”在MVCC里是怎么工作的?
在可重复读隔离级别下,事务启动时就拍了个快照,这个快照是基于整个库的。但是这个快照并不需要拷贝整个数据库的数据,具体做法如下:
InnoDB的每一个事务都有一个唯一的事务ID,叫作transaction id。它是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的。
而每一行的数据都是有多个版本。每次事务进行更新数据时,都会生成一个新的版本的数据,然后这个新数据会绑定这个事务ID,记为row trx_id。也就是说,数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id,通过这个row trx_id可以知道是哪个事务更新的数据。
一个具体的例子如下图所示:
这里V4是最新的版本,但是数据库只存了V4,它前面的版本V1和V2以及V3并不是真实存在的,而是每次需要的时候根据当前版本和undo log计算出来的。比如,需要V2的时候,就是通过V4依次执行U3、U2算出来。
按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务。但是事务执行期间,其他事务的更新对他不可见。
具体实现
InnoDB为每一个事务构造了一个数组,用来保存事务启动的瞬间,当前启动了但是还没提交的事务。
通过这个数组,我们可以将系统中的所有事务划分为3个部分,小于数组中最小ID的是已经提交了的事务,高于数组最大值是还没开始的事务,数组中的是已经开始的但是还没提交的事务。如下图所示:
针对当前事务,如果一个数据的row trx_id是在绿色区域,那么一定是可见的,如果在红色区域,则不可见。而处于黄色区域的数据,则分为两种情况,如果该row trx_id位于数组中,则说明该事务还没提交,那么就是不可见的,如果不在该数组中,说明已经提交了,则是可见的数据。这里可见得数据直接读取即可,不可见得数据是需要根据undo log进行回滚得数据。
要注意这里的一个可能的误区,这个数组并不一定是连续的,比如它可以是[50, 53, 60, 62]这种,而不连续的部分说明事务已经提交了。
通过这种设计,如果一个数据的row trx_id小于数组中得最小值,则直接按当前值读取,如果大于,则根据情况,看是否可见,如果不可见则需要根据undo log 进行回滚操作。
开头的问题
现在看开头的那个问题。假设事务A的id是100,那么事务B的id是101,事务C的id是102。假设id = 1那一行的row trx_id的值是90。
那么事务A中,事务创建的数组就只有 100,而事务B创建的数组有100,101。事务C创建的数组有100,101,102。
那么从结果上看,值为(1,1)的数据版本号为90, (1,2)数据的版本号为102(事务C修改的), (1,3)的数据版本号为101(事务B修改的)。
所以事务A在执行查询操作时,发现数据(1,3)的版本号为102,高于数组最大值,是不可见的状态,所以他要退回,一直退到(1,1)这个状态,版本号为90,小于数组中的值,是可见的,所以事务A查到的k = 1。
同理,事务B在执行查找时,发现当前数据(1,3)的版本号为101,是它自己的版本,所以直接展示k = 3。注意,这里展示k = 3是因为直接展示了最终结果,在事务B修改操作时,把k的版本号改为了事务B的id。
更新逻辑
这里针对上面事务B中的update语句将数据改为(1,3)做出解释:在事务B中,如果修改数据之前,执行一次查询操作,它读到的k的值的确是1,但此时的set k=k+1是在(1,2)的基础上进行的操作,否则它将使得事务C的修改无效,导致数据不一致。
事务的可重复读是怎么实现的
简单来说,就是读操作,需要判断当前数据版本是否可见,不可见就回滚到可见版本。而写操作,则直接在当前版本进行修改。
参考
《MySQL45讲》