事务到底是隔离的还是不隔离的?

问题:

观点一:事务T要启动视图,read-view,之后事务T执行期间,即使有其他事务修改了数据,事务T看到的仍然跟在启动时看到的一样,也就是说,一个重复读隔离级别执行下的事务,好像与世无争,不受外界影响。

观点二:但是面对行锁时:,一个事务要更新一行,如果刚好有另外一个事务拥有这一行的行锁,它又不能这么超然了,会被锁住,进入等待状态。问题是,既然进入了等待状态,那么等到这个事务自己获取到行锁要更新数据的时候,它读到的值又是什么呢?

这不是与观点一产生矛盾了?

分析:在MySQL里,有两个“视图”的概念:

一个是view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是create view … ,而它的查询方法与表一样。

另一个是InnoDB在实现MVCC时用到的一致性读视图,即consistent read view,用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现。它没有物理结构,作用是事务执行期间用来定义“我能看到什么数据”。
 

 

MySQL Innodb的MVCC实现原理

 mvcc是Multi-version concurrency control的缩写,也就是多版本并发控制。大家都知道,事务的隔离可以通过行锁来实现。在开启事务时,对操作记录加行锁,事务结束时释放锁。但是这样加锁会降低事务的并发量,并且对线程的阻塞和恢复操作也会损耗性能。那种在事务中使用了select …… for update/ lock in share mode 的就是对记录使用了行锁,实现一致性锁定读。而对于未加锁的记录,在innodb中的repeatable read级别下会通过mvcc进行并发控制,实现一致性非锁定读。

补充:MVCC   一般概念上的mvcc是通过在row record上隐式的增加两个版本号
(创建版本号和删除版本号)字段来记录数据的创建时间和删除时间。新增记录时,创建版本号填入当前事务的事务号,删除版本号置空;删除记录时,将记录标记为删除状态,并将当前事务号填入删除版本号;当进行更新记录操作时,则是先将旧记录标记为删除(并填入删除版本号),而后在插入一天新的记录,创建版本号填入当前事务号。这样在同一个事务中,不管期间有多少个其它并发事务对其查询的记录做过任何操作,都可以通过将查询出的所有记录进行版本号过滤(过滤掉创建版本号和删除版本号大于当前事务号的记录),得到与事务开启时一致的查询结果。通过mvcc,减少了对数据加锁的操作,减小性能开销,大大提高了数据库的事务并发能力。MySQL中的mvcc虽然从原理上讲和上述实现相似,但是实际的实现则略有不同,主要是体现在update操作上。

为了说明查询和更新的区别,我换一个方式来说明,把read view拆开,来更深一步地理解MVCC!

“快照”在MVCC里是怎么工作的?

事务再启动的时候就 “拍了快照” ,注意这个快照是基于整库的。

问题来了 :如果一个库有100G,那么我启动一个事务,MySQL就要拷贝100G的数据出来,这个过程得多慢啊。可是,我平时的事务执行起来很快啊

说明分析:

      我们不需要拷贝出这100G的数据。我们先来看看这个快照是怎么实现的?

 重点:
       InnoDB里面每个事务有一个唯一的事务ID,叫作transaction id。它是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的。

       而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把transaction id赋值给这个数据版本的事务ID,记为row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。
        也就是说,数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id。

看图继续说明:

                

图中虚线框里是同一行数据的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 分成了几种不同的情况。

                                               1.绿色:表示这版本是已提交的事务,数据可见;

                                                2.黄色:包括两个部分:(1) row trx_id在数组中,这个版本还没提交事务生成的;

                                                                                       (2)不在数组中,表示这个版本是已经提交了的事务生成的,可见;

                                                3. 红色:表示这个版本是由将来启动的事务生成的,是肯定不可见的;

结论:

                  所以你现在知道了,InnoDB利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。

更新逻辑:

            更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。

问题来了:

           1.要是读的都是最新的,那么读过后,再去更新写操作,那么就不会产生问题

           2.要是A读的时候是一个版本,但是B在A写操作完成一个update操作了,那么会产生什么问题?

方法:

            两阶段所协议:事务B必须等待事务A执行完后,释放完锁后,才能继续下一步操作;