28.readview的生成时机

在MySQL中,READ COMMITTED和REPEATABLE READ隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。

给事务分配id的时机

事务分为只读事务和读写事务。

只读事务

我们可以通过START TRANSACTION READ ONLY语句开启一个只读事务。

在只读事务中不可以对普通的表(其他事务也能访问到的表)进行增、删、改操作,但可以对临时表做增、删、改操作。

读写事务

我们可以通过START TRANSACTION READ WRITE语句开启一个读写事务,或者使用BEGIN、START TRANSACTION语句开启的事务默认也是读写事务。

begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表的语句。

比如一个快照读语句,事务才真正启动。如果你想要马上启动一个事务,可以使用start transaction with consistent snapshot 这个命令

在读写事务中可以对表执行增删改查操作。

分配事务的时机

如果某个事务执行过程中对某个表执行了增、删、改操作,那么InnoDB存储引擎就会给它分配一个独一无二的事务id,分配方式如下:

  • 对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增、删、改操作时才会为这个事务分配一个事务id,否则的话是不分配事务id的。

  • 对于读写事务来说,只有在它第一次对某个表(包括用户创建的临时表)执行增、删、改操作时才会为这个事务分配一个事务id,否则的话也是不分配事务id的。

    有的时候虽然我们开启了一个读写事务,但是在这个事务中全是查询语句,并没有执行增、删、改的语句,那也就意味着这个事务并不会被分配一个事务id。

    小贴士: 我们前边说过,只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。

说了半天,事务id有啥用?这个先保密哈,后边会一步步的详细唠叨。现在只要知道只有在事务对表中的记录做改动时才会为这个事务分配一个唯一的事务id。

事务id是怎么生成的

这个事务id本质上就是一个数字,它的分配策略和我们前边提到的对隐藏列row_id(当用户没有为表创建主键和UNIQUE键时InnoDB自动创建的列)的分配策略大抵相同,具体策略如下:

  • 服务器会在内存中维护一个全局变量,每当需要为某个事务分配一个事务id时,就会把该变量的值当作事务id分配给该事务,并且把该变量自增1。
  • 每当这个变量的值为256的倍数时,就会将该变量的值刷新到系统表空间的页号为5的页面中一个称之为Max Trx ID的属性处,这个属性占用8个字节的存储空间。
  • 当系统下一次重新启动时,会将上边提到的Max Trx ID属性加载到内存中,将该值加上256之后赋值给我们前边提到的全局变量(因为在上次关机时该全局变量的值可能大于Max Trx ID属性值)。

这样就可以保证整个系统中分配的事务id值是一个递增的数字。先被分配id的事务得到的是较小的事务id,后被分配id的事务得到的是较大的事务id。

ReadCommit:每次读取都生成ReadView

对于使用READ UNCOMMITTED隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了;

对于使用SERIALIZABLE隔离级别的事务来说,mysql规定使用加锁的方式来访问记录;

对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。

接下来看一下READ COMMITTED和REPEATABLE READ所谓的生成ReadView的时机不同到底不同在哪里。

RC隔离级别,实际上意思就是说你事务运行期间,只要别的事务修改数据还提交了,你就是可以读到人家修改的数据的,所以是会发生不可重复读的问题,包括幻读的问题,都会有的。

那么所谓的ReadView机制,之前我们讲过,他是基于undo log版本链条实现的一套读视图机制。

如何基于ReadView机制来实现RC隔离级别呢?

其实这里的一个非常核心的要点在于,当你一个事务设置他处于RC隔离级别的时候,他是每次发起查询,都重新生成一个ReadView!

大家注意,这点是非常重要的,接着我们通过画图一步一步来给大家演示这个RC隔离级别是怎么做到的。

首先假设我们的数据库里有一行数据,是事务id=50的一个事务之前就插入进去的,然后现在呢,活跃着两个事务,一个是事务A(id=60),一个是事务B(id=70),此时如下图所示。

image.png

现在的情况就是,事务B发起了一次update操作,更新了这条数据,把这条数据的值修改为了值B,所以此时数据的trxid会变为事务B的id=70,同时会生成一条undo log,由rollpointer来指向,看下图:

image.png

这个时候,事务A要发起一次查询操作,此时他一发起查询操作,就会生成一个ReadView,此时ReadView里的mintrxid=60,maxtrxid=71,creatortrxid=60,此时如下图所示。

image.png

这个时候事务A发起查询,发现当前这条数据的trxid是70。也就是说,属于ReadView的事务id范围之间,说明是他生成ReadView之前就有这个活跃的事务【活跃说明事务并未提交,还可以继续修改或者回滚】,是这个事务修改了这条数据的值,但是此时这个事务B还没提交,所以ReadView的mids活跃事务列表里,是有[60, 70]两个id的,所以此时根据ReadView的机制,此时事务A是无法查到事务B修改的值B的。

接着就顺着undo log版本链条往下查找,就会找到一个原始值,发现他的trxid是50,小于当前ReadView里的mintrx_id,说明是他生成ReadView之前,就有一个事务插入了这个值并且早就提交了,因此可以查到这个原始值,如下图。

image.png

接着,咱们假设事务B此时就提交了,好了,那么提交了就说明事务B不会活跃于数据库里了,是不是?可以的,大家一定记住,事务B现在提交了。那么按照RC隔离级别的定义,事务B此时一旦提交了,说明事务A下次再查询,就可以读到事务B修改过的值了,因为事务B提交了。

那么到底怎么让事务A能够读到提交的事务B修改过的值呢?

很简单,就是让事务A下次发起查询,再次生成一个ReadView。此时再次生成ReadView,数据库内活跃的事务只有事务A了,因此mintrxid是60,mactrxid是71,但是mids这个活跃事务列表里,只会有一个60了,事务B的id=70不会出现在mids活跃事务列表里了,如下图。

image.png

此时事务A再次基于这个ReadView去查询,会发现这条数据的trxid=70,虽然在ReadView的mintrxid和maxtrxid范围之间,但是此时并不在mids列表内,说明事务B在生成本次ReadView之前就已经提交了。

那么既然在生成本次ReadView之前,事务B就已经提交了,就说明这次你查询就可以查到事务B修改过的这个值了,此时事务A就会查到值B,如下图所示。

image.png

到此为止,RC隔离级别如何实现的,大家应该就理解了,他的关键点在于每次查询都生成新的ReadView,那么如果在你这次查询之前,有事务修改了数据还提交了,你这次查询生成的ReadView里,那个m_ids列表当然不包含这个已经提交的事务了,既然不包含已经提交的事务了,那么当然可以读到人家修改过的值了。

这就是基于ReadView实现RC隔离级别的原理,希望大家好好仔细去体会,实际上,基于undo log多版本链条以及ReadView机制实现的多事务并发执行的RC隔离级别、RR隔离级别,就是数据库的MVCC多版本并发控制机制。

他本质是协调你多个事务并发运行的时候,并发的读写同一批数据,此时应该如何协调互相的可见性。

56 ReadRepeat:第一次读取生成ReadView

在MySQL中让多个事务并发运行的时候能够互相隔离,避免同时读写一条数据的时候有影响,是依托undo log版本链条和ReadView机制来实现的。

基于ReadView机制可以实现RC隔离级别,即你每次查询的时候都生成一个ReadView。

这样的话,只要在你这次查询之前有别的事务提交了,那么别的事务更新的数据,你是可以看到的。

在RR级别下,你这个事务读一条数据,无论读多少次,都是一个值,别的事务修改数据之后哪怕提交了,你也是看不到人家修改的值的,这就避免了不可重复读的问题。

同时如果别的事务插入了一些新的数据,你也是读不到的,这样你就可以避免幻读的问题。

那么到底是如何实现的呢?我们今天来看看。

首先我们还是假设有一条数据是事务id=5的一个事务插入的,同时此时有事务A和事务B同时在运行,事务A的id是60,事务B的id是70,如下图所示

image.png

这个时候,事务A发起了一个查询,他就是第一次查询就会生成一个ReadView,此时ReadView里的creatortrxid是60,mintrxid是60,maxtrxid是71,m_ids是[60, 70],此时ReadView如下图所示。

image.png

这个时候事务A基于这个ReadView去查这条数据,会发现这条数据的trxid为50,是小于ReadView里的mintrx_id的,说明他发起查询之前,早就有事务插入这条数据还提交了,所以此时可以查到这条原始值的,如下图。

image.png

接着就是事务B此时更新了这条数据的值为值B,此时会修改trx_id为70,同时生成一个undo log,而且关键是事务B此时他还提交了,也就是说此时事务B已经结束了,如下图所示。

image.png 这个时候大家思考一个问题,ReadView中的m_ids此时还会是60和70吗?

那必然是的,因为ReadView一旦生成了就不会改变了,这个时候虽然事务B已经结束了,但是事务A的ReadView里,还是会有60和70两个事务id。

他的意思其实就是,在你事务A开启查询的时候,事务B当时是在运行的,就是这个意思。

那么好,接着此时事务A去查询这条数据的值,他会惊讶的发现此时数据的trxid是70了,70一方面是在ReadView的mintrxid和maxtrxid的范围区间的,同时还在mids列表中

这说明什么?

说明起码是事务A开启查询的时候,id为70的这个事务B还是在运行的,然后由这个事务B更新了这条数据,所以此时事务A是不能查询到事务B更新的这个值的,因此这个时候继续顺着指针往历史版本链条上去找,如下图。

image.png

接着事务A顺着指针找到下面一条数据,trxid为50,是小于ReadView的mintrx_id的,说明在他开启查询之前,就已经提交了这个事务了,所以事务A是可以查询到这个值的,此时事务A查到的是原始值,如下图。

image.png

大家看到这里有什么感想?是不是感觉到这一下子就避免了不可重复读的问题?

你事务A多次读同一个数据,每次读到的都是一样的值,除非是他自己修改了值,否则读到的一直会一样的值。

不管别的事务如何修改数据,事务A的ReadView始终是不变的,他基于这个ReadView始终看到的值是一样的!

接着我们来看看幻读的问题他是如何解决的。假设现在事务A先用select * from x where id>10来查询,此时可能查到的就是一条数据,而且读到的是这条数据的原始值的那个版本,至于原因,上面都解释过了,如下图。

image.png

现在有一个事务C插入了一条数据,然后提交了,此时如下图所示。

image.png

接着,此时事务A再次查询,此时会发现符合条件的有2条数据,一条是原始值那个数据,一条是事务C插入的那条数据,但是事务C插入的那条数据的trxid是80,这个80是大于自己的ReadView的maxtrx_id的,说明是自己发起查询之后,这个事务才启动的,所以此时这条数据是不能查询的。

因此事务A本次查询,还是只能查到原始值一条数据,如下图。

image.png

所以大家可以看到,在这里,事务A根本不会发生幻读,他根据条件范围查询的时候,每次读到的数据都是一样的,不会读到人家插入进去的数据,这都是依托ReadView机制实现的!

我们之前说执行DELETE语句或者更新主键的UPDATE语句。并不会立即把对应的记录完全从页面中删除,而是执行一个所谓的delete mark操作,相当于只是对这行记录打上了一个删除标志位,这主要就是为MVCC服务的,大家可以对比上边举的例子自己试想一下怎么使用。

另外,所谓的MVCC只是在我们进行普通的SEELCT查询时才生效,截止到目前我们所见的所有SELECT语句都算是快照读.

参考:https://blog.csdn.net/zht245648124/category119754892.html

参考:https://juejin.cn/book/6844733769996304392?enterfrom=searchresult&utm_source=search

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值