场景
- 可重复读隔离级别,在启动事务时创建一个视图read-view;之后事务执行期间,即使其他事务修改了数据,本事务看到的仍然和启动时看到的一样。
- 一个事务要更新一行,如果刚好有另一个事务拥有这一行行锁,会造成锁等待。
- 问题:当这个事务拿到锁的时候,他读到的数据到底是什么呢?
事务启动
- begin或start transaction时并不是真正的起点,而是其后面第一个操作InnoDB表的语句,事务才真正启动;可重复读隔离级别下,第一个
快照读
语句会创建视图。 - 如果想要马上启动事务,可以使用
start transaction with consistent snapshot
,此时直接创建视图。 快照读:读取的是快照版本,也就是历史版本。普通的SELECT就是快照读。
当前读:读取的是最新版本。UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE是当前读。
- 图片来自Mysql45讲08 , 初始k=1
- 结果B事务读到的是3,A事务读到的是1;因为A的视图是在更新前创建,B的视图是在select时创建。
视图
- 一个是
view
,是用查询语句定义的的虚拟表,create view… - 第二个是InnoDB在实现MVCC(多版本并发控制)时用到的一致性读视图,即consistent read view,用于支持读提交和可重复读隔离级别的实现。作用是事务执行期间用来定义
能看到什么数据
。
快照
- 在可重复读隔离级别下,事务在启动时拍了个快照,这个快照是基于整个库的。
- 问题:假设库有100G,快照不是复制,那是怎么实现的呢?
实现方法
- InnoDB里面每个事务有一个唯一的事务ID,叫做transaction id。是在事务开始时向InnoDB事务系统申请的,是按照申请顺序严格递增的。
- 每行数据是有多个版本的,每次事务更新数据的时候,都会生成一个新的数据版本。并且把transaction id赋值给这个数据版本的事务ID,记为row trx_id。旧版本保留。所以数据库中的一行记录,可能有多个版本,每个版本的row trx_id就是更新出这个版本的事务的transaction id。
- 图片来自Mysql45讲08
- 语句更新会生成回滚日志(undo log),即图中的虚线箭头。
- V1、V2、V3并不是真实存在的的,而是需要当前版本和回滚日志计算出来的。比如,需要V2时是通过V4依次执行U3和U2得到的。
- 按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果,但是之后,这个事务执行期间,其他事务的更新对它不可见。
- 因此一个事务只需要在启动时声明说,以我启动的时刻为准,如果数据版本是在我启动前生成的,就认;否则就不认,必须找到它的上一个版本。
- InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在活跃的所有事务ID(
数组最后一个值是本事务id
),活跃指的就是,启动了还没提交。数组里面事务ID的最小值记为低水位,当前系统里面创建过的事务ID最大值加1记为高水位。 - 这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。
- 而数据版本的可见性规则,就是基于数据的row trx_id和这个一致性视图的对比结果得到的。
- 图片来自Mysql45讲08
- 这样对于当前事务的启动瞬间来说,一个数据版本的row trx_id(即更新出这个数据版本的事务transaction id),有以下几种可能;
- 落在绿色部分,该数据版本可见,因为在启动前就提交了。
- 落在红色部分,肯定不可见。
- 落在黄色部分,若row trx_id在数组中,表示该版本是由本事务启动瞬间,那些还未提交的事务生成的,当然不可见。
- 落在黄色部分,若row trx_id不在数组中,表示该数据版本是由
在低水位事务后申请,在本事务启动前就已经提交
,所以可见。 - InnoDB利用了“所有数据都有多个版本”这个特性,实现了秒级创建快照的能力。
解答
-
为什么 A 读到1 ?
-
设A,B,C的transaction id分别是 100,101,102。A的视图数组是[100],即只有他自己。低水位是100,高水位是101
-
C事务更新的数据版本是102,B事务更新的数据版本是101
-
A的读取流程是先从当前版本读起,即101版本,判断大于等于高水位,位于红色区,不可见。再读数据的上一版本,102,大于高水位,依旧不可见。在读之前一版本,即更新k=1的事务,row trx_id肯定小于100,可见。
-
对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:版本未提交,不可见;版本已提交,但是是在视图数组创建后提交的,不可见;版本已经提交,且是在视图创建前提交的,可见。
更新逻辑
- 更新时,不能在历史版本更新,否则事务C的更新就丢失了;
- 更新数据都是先读后写的,这个读只能读当前值,因此B更新时当前读拿到的k=2,再更新得到k=3,这个k=3版本的 row trx_id 是101
- 所以B查询时,B的视图数组是,[100,101]。低水位100,高水位102。读当前版本是101,该版本是由自己更新,所以认了。
- 如果A使用select … lock in share mode(读锁)或select … for update(写锁),也属于当前读,也是可以读到版本号是101的数据即k=3。
总结
- InnoDB 的行数据有多个版本,每个数据版本有自己的 row trx_id,每个事务或者语句有自己的一致性视图。
可重复读的核心是一致性读;事务更新数据时,只能使用当前读,如果当前记录的行锁被其他事务占用,就需要进入锁等待。
在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图。
在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。
- 普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性。
- 对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
- 对于读提交,查询只承认在语句启动前就已经提交完成的数据;
- 而当前读,总是读取已经提交完成的最新版本。