MySQL 笔记整理(8.a) --事务到底是隔离还是不隔离的?
笔记记录自林晓斌(丁奇)老师的《mysql实战45讲》
8.a) --事务到底是隔离还是不隔离的?
这部分内容不太容易理解,笔者也是进行了多次阅读。因此引用原文:
之前有提到过,如果是在可重复读隔离级别,事务t启动的时候会创建一个视图read-view,之后事务t执行期间,即使有其他事务修改了数据,事务t看到的仍然跟在启动时看到的一样,也就是说,一个在可重复读隔离级别下执行的事务,好像与世无争,不受外界影响。
但是,在之前也行锁相关内容时又提到,一个事务要更新一行,如果刚好有另外一个事务拥有这一行的行锁,它又不能这么超然了,会被锁住,进入等待状态。问题是,既然进入了等待状态,那么等到事务自己获取到行锁要更新数据的时候,它读到的值又是什么呢?
举一个例子,下面是一个只有两行的表的初始化语句。
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);
图一, 事务a, b, c的执行流程
这里我们需要注意的是事务的启动时机。
begin/start transaction命令并不是一个事务的起点,在执行到它们之后的第一个操作innodb表的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用start transaction with consistent snapshot命令。
- 第一种启动方式,一致性视图是在执行第一个快照读语句时创建的。
- 第二种启动方式,一致性视图是在执行start transaction with consistent snapshot时创建的。
还需要注意,没有特殊说明的情况下,我们默认autocommit=1.
在上面这个例子中,事务c没有显示的使用begin/commit,表示这个update语句本身就是一个事务,语句完成时会自动提交。事务b在更新了行之后查询;事务a在一个只读事务中查询,并且时间顺序上是在事务b的查询之后。这时候,如果告诉你事务b查询到的k值是3,而事务a查到的k的值是1,你是不是感觉有点晕呢?
所以今天这篇文章就是想和你说明这个问题。在mysql里有两个视图概念:
- 一个是view,它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是create view...,而它的查询方法与表一样。
- 另一个是innodb在实现mvcc时用到的一致性视图,即consistent read view,用于支持rc(read commited 读提交)和rr(repeatable read,可重复读)隔离级别的实现。
它没有物理结构,作用是事务执行期间用来定义“我能看到什么数据”.
“快照”在mvcc里是怎么工作的?
在可重复读的隔离级别下,事务在启动的时候就“拍了个快照”,注意,这个快照是基于整个数据库的。这时,你可能觉得这不太现实,毕竟如果一个库有100g,那么我启动一个事务,mysql就要拷贝100g的数据,这个过程得多慢啊。可是平常执行事务却很快。实际上,并不需要拷贝这100g的数据。我们先来看看这个快照是怎么实现的。
innodb里面每个事务有一个唯一的事务id,叫做transaction id。它是在事务开始的时候向innodb的事务系统申请的,是按申请顺序严格递增的。
而且,每行数据也是有多个版本的,每次事务更新的时候,都会生成一个新的数据版本,并且把transaction id 赋值给这个数据版本的事务id,记为row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。也就是说,数据表中的每一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id。
如图2所示,就是一个记录被多个事务连续更新后的状态。
图2 行状态变更图
图中虚线框里是同一行数据的4个版本,当前最新版本是v4,k的值是22,它是被transaction id为25的事务更新的,因此它的row trx_id也是25.你可能会问,前面的文章不是说,语句更新会生成undo log(回滚日志)吗?那么,undo log在哪儿呢?实际上,图2中的三个虚线箭头,就是undo log;而v1,v2,v3并不是物理上真实存在的,而是每次需要的时候根据当前版本和undo log计算出来的。比如,需要v2的时候,就是通过v4依次执行u3,u2算出来。明白了多版本和row trx_id的概念后,我们再来想一下,innodb是怎么定义那个“100g”的快照的。按照可重复读的定义,一个事务启动的时候,能够看见所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。
因此,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本在我启动之前生成,就认;否则就不认。必须要找到它的上一个版本。当然,如果‘上一个版本’也不可见,那就得继续往前找”。除此之外,如果这个事务自己更新的数据,它自己还是要认的。在实现上,innodb为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务id。“活跃”指的就是,启动了但还没提交。数组里面事务id的最小值记为低水位,当前系统里面已经创建过的事务id的最大值加1记为搞水位。这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。而数据版本的可见性规则,就是基于数据的row trx_id和这个一致性视图的对比结果得到的。这个视图数组把所有的 row trx_id分成了几种不同的情况。
图3, 数据版本可见性规则
这样,对于当期事务的启动瞬间来说,一个数据版本的row trx_id,有以下几种可能:
- 如果落在绿色部分,表示这个版本是已提交的事务或者是当期事务自己生成的,这个数据是可见的。
- 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的。
- 如果落在黄色部分,那就包括两种情况。a. 若row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见。b.若 row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。
所以你现在知道了,innodb利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。接下来我们回顾一下图1中的三个事务,分析事务a的语句返回的结果,为什么是k=1.
未完待续.....
上一篇: MySQL 安装