MySQL事务隔离-下
本文内容是整理极客时间的mysql课程。
-
事务的隔离级别,如果是可重复读的隔离界别,事务T启动的时候会创建一个视图read-view,之后事务T执行期间,即使有其他事务修改了数据,事务T看到的数据和在启动的时候看到的一样。
-
如果这个数据表中有行锁,一个事务A要更新一行,但是这行数据的行锁被另外一个事务B占有了,事务A就被锁住了,进入等待状态。那么等待事务A拿到行锁进行数据更新的时候,它读到的值是什么?事务B查到的K值是3,事务A查到的k值是1。
-
为理解上述问题,举一个下面的例子,下面是一个只有两行的表的初始化语句。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);
-
事务启动命令bfging/start transaction 命令并不是一个事务的起点,在执行到他们之后的第一个操作InnoDB表的语句,事务才真正启动。start transaction with snapshot 这个命令是马上启动一个事务,也就是马上建立一个快照。如果事务没有显式的使用begin/commit,表示这个update语句本身就是一个事务,语句完成的时候自动提交。
-
MySQL里的两个“视图”的概念
-
1、view,他是一个用查询语句定义的虚拟表,调用的时候用查询语句生成的。创建视图的语法是create view... ,而它的查询方法与表一样。
-
2、InnoDB在实现MVCC时用到的一致性视图,即 consistent read view,用于支持RC(read committed,读提交)和RR(repeatable read,可重复读)隔离级别的实现。它没有物理结构,用于事务执行期间定义“我能看到什么”
-
“快照”在MVCC里是怎么工作的?
-
在可重复读的隔离级别下,事务在启动的时候就“拍了个快照”,注意,这个快照是基于整个库的。
-
“快照”的实现InnoDB里面每个事务有一个唯一的事务ID,叫做transaction id。它是事务在开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的。而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。
-
一个记录被多个事务连续更新后的状态。
-
undo log(回滚日志),语句更新生成的undo log日志,图2中的三个虚线箭头,就是undo log。v1,v2,v3并不是物理上真实存在的,而是每次需要的时候根据当前版本和undo log计算出来的。
-
一个事务启动后,其他事务操作的数据哪些是他能看见的,哪些是他看不见的?
-
理解这个之前,先要知道“视图数组”InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正”在活跃“的所有事务ID,"活跃"指的是,启动了但是还没有提交。数组里存的是事务的ID,里面事务ID的最小值标记为低水位,里面事务ID的最大值+1标记为高水位。视图一致性的问题就靠这个“视图数组“解决了
-
视图数组把所有row trx_id 分成了几种不同的情况,(row trx_id 是某行数据的版本号,由transaction id 赋值产生)
-
对于当前事务的启动瞬间来说,一个数据版本的row trx_id,有以下几种可能:1、如果落在绿色部分,数据是可见的,表示这个版本是在当前事务启动之前提交的,或者是当前事务自己生成的。2、如果落在红色部分,数据不可见的,表示这个版本是将来启动的事务生成的。3、如果落在黄色部分,那就是包括两种情况。a、诺row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见;b、诺row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见;
-
事务A查询数据逻辑图
-
事务A查询语句的读书节流程是:1、找到(1,3)的时候,判断出row trx_id =101,比高水位大,处于红色区域,不可见;2、接着,找到上一个历史版本,一看row trx_id =102,比高水位大,处于红色区域不可见,3、再往前找,找到(1,1),他的row trx_id= 90,比低水位小,处于绿色区域,可见。
-
对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:1、版本未提交,不可见;2、版本已提交,但是是在视图创建后提交的,不可见;3、版本已提交,而且是在视图创建前提交的,可见
-
更新逻辑
-
事务B更新逻辑图
-
更新数据都是先读后写的,而这个读,只能读当前的值,称为”当前读“(current read)。
-
除了update语句以外,select语句如果加锁,也是当前读。如果把事务 A 的查询语句 select * from t where id=1 修改一下,加上 lock in share mode 或 for update,也都可以读到版本号是 101 的数据,返回的 k 的值是 3。下面这两个 select 语句,就是分别加了读锁(S 锁,共享锁)和写锁(X 锁,排他锁)。
-
假设事务C不是马上提交而是,变成如下图所示的事务C',会怎样?事务B启动后,事务C'启动,并更新了k的值,在事务C'还没有提交的时候(事务C'没提交,也就是(1,2)这个版本的写锁没有释放),而事务B要更新k的值,首先事务B要读取到最新版本(因为事务B是update的),而且要加锁,这是就被锁住了,必须等事务C’释放这个锁,才能继续它当前读。
-
事务B更新逻辑图
-
可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。
-
读提交和可重复读的逻辑类似,主要区别是:1、在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后的事务里的其他查询都共用这个一致性视图;2、在读提交隔离级别下,每一个语句执行前都会重新计算出一个新的视图。
-
在读提交隔离级别下,事务A和事务B的查询语句查到的k,分别应该是多少?注意:”start transaction with consistent snapshot;"的意思是从这个语句开始,创建一个持续整个事务的一致性快照。所以在读提交隔离级别下,这个用法就没意义了。等效于普通的start transaction。
-
读提交隔离级别下的事务状态图这时,事务A的查询语句的视图数组是在执行这个语句的时候创建的,时序上(1,2)、(1,3)的生成时间都在创建这个视图数组的时刻之前。但是在这个时刻:1、(1,3)还没提交,属于情况1,不可见;2、(1,2)提交了,属于情况3,可见;所以事务A查询语句返回的是k=2;事务B查询的结果是k=3;
-
小结
-
InnoDB 的行数据有多个版本,每个数据版本有自己的 row trx_id,每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性。
-
对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
-
对于读提交,查询只承认在语句启动前就已经提交完成的数据;
-
因为表结构没有对应的行数据,也没有 row trx_id,因此只能遵循当前读的逻辑。