事务不同的启动方式创建视图的时机不同:
- begin/start transaction:一致性视图是在第执行第一个快照读语句时创建的;
- start transaction with consistent snapshot:在执行这行语句时一致性视图创建的。
MySQL中视图的概念:
- 一个是view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是create view … ,而它的查询方法与表一样。
- InnoDB在实现MVCC时用到的一致性读视图(consistent read view),用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现。
1 MVCC中"快照"的工作机制
1.1 "快照"的实现方式
在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。
transaction id 和 row trx_id:
- transaction id:InnoDB里面每个事务唯一的事务ID,在事务开始的时候向InnoDB的事务系统申请的,严格递增的;
- row trx_id:每次事务更新数据的时候,都会生成一个新的数据版本,并且把transaction id赋值给这个数据版本的事务ID,这个ID记为row trx_id。
数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id,如下图所示,一个记录被多个事务连续更新的情况:
图中虚线框里是同一行数据的4个版本,当前最新版本是V4,k的值是22,它是被transaction id 为25的事务更新的,因此它的row trx_id也是25。
undo log:图中的三个虚线箭头就是undo log,V1、V2、V3不是物理真实存在的,每当需要时根据当前版本和undo log计算得出。
1.2 事务的可见性实现
可重复读:一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。
如何实现的:
-
InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID(启动了但还没提交)。
-
视图数组把所有的row trx_id 分成了几种不同的情况:
对于当前事务的启动瞬间来说,一个数据版本的row trx_id有下面几种情况:
- 在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,数据可见;
- 在红色部分,表示这个版本是由将来启动的事务生成的,数据不可见;
- 在黄色部分,
- 若row trx_id在数组中,表示这个版本是由还没提交的事务生成的,数据不可见;
- 若row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,数据可见。
一致性读
有一个表数据如下所示:
mysql> CREATE TABLE `t` (
`id` int(11) NOT NULL,
`k` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);
比如如下图3个事务操作:
做3条假设:
- 事务A开始前,系统里面只有一个活跃事务ID是99;
- 事务A、B、C的版本号分别是100、101、102,且当前系统里只有这四个事务;
- 三个事务开始前,(1,1)这一行数据的row trx_id是90。
所以,事务A的视图数组就是**[99,100], 事务B的视图数组是[99,100,101], 事务C的视图数组是[99,100,101,102]**,如下图所示
事务A查询语句的读数据流程:
- 找到(1,3)的时候,判断出row trx_id=101,比高水位大,处于红色区域,不可见;
- 接着,找到上一个历史版本,row trx_id=102,比高水位大,处于红色区域,不可见;
- 再往前找,row trx_id=90,比低水位小,处于绿色区域,可见。
事务A无论在什么时候查询,即使这行数据被修改过,看到这行数据的结果都是一致的,这种被称为一致性读。
总结下规律:
一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:
- 版本未提交,不可见;
- 版本已提交,但是是在视图创建后提交的,不可见;
- 版本已提交,而且是在视图创建前提交的,可见。
用这个规则再看这个例子:
在可重复读情况下,事务A创建完视图做查询时:
- (1,3)还未提交,不可见;
- (1,2)已经提交了,但是在视图数组创建之后,不可见;
- (1,1)是在视图数组创建前提交,可见。
所以事务A中查询结果还是1。
2 更新操作
2.1 当前读
事务B和事务C做更新操作,若事务B中在更新完后再做查询,拿到的值会是多少?如下图所示:
更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。
事务B的操作:
- 更新时进行当前读,拿到的数据是(1,2),更新后变成(1,3),新版本的row trx_id是101;
- 查询时,发现自己版本号为101,最新数据版本也是101,这时就可以直接使用,查询到的k值就是3。
当前读的场景:
-
update语句;
-
select语句加锁:
-
select k from t where id=1 lock in share mode;
读锁;
-
select k from t where id=1 for update;
写锁。
-
2.2存在行锁场景的当前读
有A,B,C三个事务,与上面例子唯一区别是,事务C’更新后不立刻提交,在它提交件事务B的更新语句先执行。如下图所示
这时事务B会怎样处理?
事务B是当前读,必须读最新数据,因为事务C’事务还未提交,根据两阶段锁协议,该行数据会被锁住,事务B的更新操作就必须等到事务C’释放行锁才能继续当前读。
2.3读已提交与可重复读的可见性区别
相同点:
-
两者在一致性读上都符合:
一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:
- 版本未提交,不可见;
- 版本已提交,但是是在视图创建后提交的,不可见;
- 版本已提交,而且是在视图创建前提交的,可见。
-
两者在更新数据时:
都只能当前读,并且如果当前记录的行锁被其他事务占用,就必须进入锁等待。
区别:创建视图的时机不一样
- 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
- 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。
举例:
在读已提交隔离级别下,事务A,B读到的值是多少,如下图所示:
事务A的查询语句的视图数组是在执行这个语句的时候创建的,
- 事务B的更新还未提交,不可见;
- 事务C的更新已提交,可见。
所以,A查询返回k=2,事务B在更新后查询属于当前读,所以k=3。