MVCC整理

1 简介

MVCC(Multi-Version Concurrency Control)多版本并发控制,是⽤来在数据库中控制并发的⽅法,实现对数据库的并发访问⽤的,就是⼀种写时复制的思想的应⽤。在MySQL中,MVCC只在读取已提交(Read Committed)和可重复读(Repeatable Read)两个事务级别下有效。其是通过Undo⽇志中的版本链和ReadView⼀致性视图来实现的。MVCC就是在多个事务同时存在时,SELECT语句找寻到具体是版本链上的哪个版本,然后在找到的版本上返回其中所记录的数据的过程。
⾸先需要知道的是,在MySQL中,会默认为我们的表后⾯添加三个隐藏字段:

  • **DB_ROW_ID:**⾏ID,MySQL的B+树索引特性要求每个表必须要有⼀个主键。如果没有设置的话,会⾃动寻找第⼀个不包含NULL的唯⼀索引列作为主键。如果还是找不到,就会在这个DB_ROW_ID上⾃动⽣成⼀个唯⼀值,以此来当作主键(该列和MVCC的关系不⼤);
  • **DB_TRX_ID:**事务ID,记录的是当前事务在做INSERT或UPDATE语句操作时的事务ID(DELETE语句被当做是UPDATE语句的特殊情况,后⾯会进⾏说明);
  • **DB_ROLL_PTR:**回滚指针,通过它可以将不同的版本串联起来,形成版本链。相当于链表的next指针。

2 ReadView

ReadView⼀致性视图主要是由两部分组成:所有未提交事务的ID数组和已经创建的最⼤事务ID组成(实际上ReadView还有其他的字段,但不影响这⾥对MVCC的讲解)。⽐如:[100,200],300。事务100和200是当前未提交的事务,⽽事务300是当前创建的最⼤事务(已经提交了)。当执⾏SELECT语句的时候会创建ReadView,但是在读取已提交和可重复读两个事务级别下,⽣成ReadView的策略是不⼀样的:读取已提交级别是每执⾏⼀次SELECT语句就会重新⽣成⼀份ReadView,⽽可重复读级别是只会在第⼀次SELECT语句执⾏的时候会⽣成⼀份,后续的SELECT语句会沿⽤之前⽣成的ReadView(即使后⾯有更新语句的话,也会继续沿⽤)。
在这里插入图片描述

在这里插入图片描述

3 版本链

在MySQL的多版本并发控制(MVCC)中,每个事务都可以看到数据库的一个特定版本,这些版本组成了一个版本链。版本链是一种管理并发访问的机制,用于实现不同事务之间的隔离性。

  • 当一个事务开始时,它会被分配一个唯一的事务ID,并且在数据库中的每个被修改的行都会记录该事务ID以及修改的版本号。每个版本都有一个唯一的时间戳,用于表示该版本的创建时间。
  • 在可重复读隔离级别下,当一个事务开始后,会创建一个事务开始时刻的快照(readview),该快照由当前活动的事务列表和对应的版本号组成。这个快照形成了事务在其执行期间看到的数据库的一致状态。
  • 版本链中的每个版本都与一个事务ID相关联。当一个事务读取数据时,它只会看到在它开始之前提交的版本,也就是早于它开始时间戳的版本。这样,事务之间的读写操作就可以保持隔离性,彼此不会相互干扰。
  • 当一个事务更新或删除数据时,它会创建一个新版本,并将新版本的信息添加到版本链中。其他事务仍然可以继续读取旧版本的数据,直到新版本被提交。
  • 通过版本链的管理,MySQL可以在保持数据一致性的同时实现并发性。不同事务之间的读写操作可以并发进行,只有在提交时才会引发冲突,从而提高数据库的吞吐量和性能。
  • 所有版本的数据都只会存⼀份,然后通过回滚指针连接起来,之后就是通过⼀定的规则找到具体是哪个版本上的数据就⾏了。假设现在有⼀张account表,其中有id和name两个字段,那么版本链的示意图如下:
    在这里插入图片描述
    ⽽具体版本链的⽐对规则如下,⾸先从版本链中拿出最上⾯第⼀个版本的事务ID开始逐个往下进⾏⽐对:
    在这里插入图片描述
    (其中min_id指向ReadView中未提交事务数组中的最⼩事务ID,⽽max_id指向ReadView中的已经创建的最⼤事务ID)
    1、如果落在绿⾊区间(DB_TRX_ID < min_id):这个版本⽐min_id还⼩(事务ID是从⼩往⼤顺序⽣成的),说明这个版本在SELECT之前就已经提交了,所以这个数据是可⻅的。或者(这⾥是短路或,前⾯条件不满⾜才会判断后⾯这个条件)这个版本的事务本身就是当前SELECT语句所在事务的话,也是⼀样可⻅的;
    2、如果落在红⾊区间(DB_TRX_ID > max_id):表示这个版本是由将来启动的事务来⽣成的,当前还未开始,那么是不可⻅的;
    3、 如果落在⻩⾊区间(min_id <= DB_TRX_ID <= max_id):这个时候就需要再判断两种情况:
  • 如果这个版本的事务ID在ReadView的未提交事务数组中,表示这个版本是由还未提交的事务⽣成的,那么就是不可⻅的;
  • 如果这个版本的事务ID不在ReadView的未提交事务数组中,表示这个版本是已经提交了的事务⽣成的,那么是可⻅的。
    如果在上述的判断中发现当前版本是不可⻅的,那么就继续从版本链中通过回滚指针拿取下⼀个版本来进⾏上述的判断。

4 演示过程

下⾯通过⼀个示例来具体演示MVCC的执⾏过程(假设是在可重复读事务级别下),当前account表中已经有了⼀条初始数据(id=1,name=monkey):
注:只在第一次查询时就产生readview,后续沿用
在这里插入图片描述
在这里插入图片描述
从左往右分别是五个事务,从上到下是时刻点。其中在第2和3时刻点中事务100和事务200(这⾥两个事务之间相差100只是为了更加⽅便去看,正常来说下个事务的ID是以+1的⽅式来创建的)分别执⾏了⼀条UPDATE语句,这两条语句并⽆实际作⽤,只是为了⽣成事务ID的,所以在下⾯的MVCC执⾏过程中就不分析这两条语句所带来的影响了,我们只研究account表。⽽其中最后两个事务,我是注明没有事务ID的。因为事务ID是执⾏⼀条更新操作(增删改)的语句后才会⽣成(这也是事务100和事务200要先执⾏⼀条更新语句的意义),并不是开启事务的时候就会⽣成。最后两个事务中可以看到就是执⾏了⼀些SELECT语句⽽已,所以它们并没有事务ID。
⾸先来看⼀下初始状态时的版本链和ReadView(ReadView此时还未⽣成):
在这里插入图片描述
其中事务1在account表中创建了⼀条初始数据。
之后在第1时刻点,五个事务分别开启了事务(如上所说,这个时候还没有⽣成事务ID)。
在第2时刻点,第⼀个事务执⾏了⼀条UPDATE语句,⽣成了事务ID为100。
在第3时刻点,第⼆个事务执⾏了⼀条UPDATE语句,⽣成了事务ID为200。
在第4时刻点,第三个事务执⾏了⼀条UPDATE语句,将account表中id为1的name改为了monkey301。同时⽣成了事务ID为300。
在第5时刻点,事务300也就是上⾯的事务执⾏了commit操作。
在第6时刻点,第四个事务执⾏了⼀条SELECT语句,想要查询⼀下当前id为1的数据(如上所说,该事务没有⽣成事务ID)。此时的版本链和ReadView如下:
在这里插入图片描述
因为在第5时刻点,事务300已经commit了,所以ReadView的未提交事务数组中不包含它。此时根据上⾯所说的⽐对规则,拿版本链中的第⼀个版本的事务ID为300进⾏⽐对,⾸先当前这条SELECT语句没有在事务300中进⾏查询,然后发现是落在⻩⾊区间,⽽且事务300也没有在ReadView的未提交事务数组中,所以是可⻅的。即此时在第6时刻点,第四个事务所查找到的结果是monkey301。
在第7时刻点,事务100执⾏了⼀条UPDATE语句,将account表中id为1的name改为了monkey101。
在第8时刻点,事务100⼜执⾏了⼀条UPDATE语句,将account表中id为1的name改为了monkey102。
在第9时刻点,第四个事务执⾏了⼀条SELECT语句,想要查询⼀下当前id为1的数据。此时的版本链和ReadView如下:
在这里插入图片描述
注意,因为当前是在可重复读的事务级别下,所以此时的ReadView沿⽤了在第6时刻点⽣成的ReadView(如果是在读取已提交的事务级别下,此时就会重新⽣成⼀份ReadView了)。然后根据上⾯所说的⽐对规则,拿版本链中的第⼀个版本的事务ID为100进⾏⽐对,⾸先当前这条SELECT语句没有在事务100中进⾏查询,然后发现是落在⻩⾊区间,⽽且事务100是在ReadView的未提交事务数组中,所以是不可⻅的。此时通过回滚指针拿取下⼀个版本,发现事务ID仍然为100,经过分析后还是不可⻅的。此时⼜拿取下⼀个版本:事务ID为300进⾏⽐对,⾸先当前这条SELECT语句没有在事务300中进⾏查询,然后发现是落在⻩⾊区间,但是事务300没有在ReadView的未提交事务数组中,所以是可⻅的。即此时在第9时刻点,第四个事务所查找到的结果仍然是monkey301(这也就是可重复读的含义)。
在第10时刻点,事务100commit提交事务了。同时事务200执⾏了⼀条UPDATE语句,将account表中id为1的name改为了monkey201。
在第11时刻点,事务200⼜执⾏了⼀条UPDATE语句,将account表中id为1的name改为了monkey202。
在第12时刻点,第四个事务执⾏了⼀条SELECT语句,想要查询⼀下当前id为1的数据。此时的版本链和ReadView如下
在这里插入图片描述
跟第9时刻点⼀样,在可重复读的事务级别下,ReadView沿⽤了在第6时刻点⽣成的ReadView。然后根据上⾯所说的⽐对规则,拿版本链中的第⼀个版本的事务ID为200进⾏⽐对,⾸先当前这条SELECT语句没有在事务200中进⾏查询,然后发现是落在⻩⾊区间,⽽且事务200是在ReadView的未提交事务数组中,所以是不可⻅的。此时通过回滚指针拿取下⼀个版本,发现事务ID仍然为200,经过分析后还是不可⻅的。此时⼜拿取下⼀个版本:事务ID为100进⾏⽐对,⾸先当前这条SELECT语句没有在事务100中进⾏查询,然后发现是落在⻩⾊区间内,同时在ReadView的未提交数组中,所以依然是不可⻅的。此时⼜拿取下⼀个版本,发现事务ID仍然为100,经过分析后还是不可⻅的。此时再拿取下⼀个版本:事务ID为
300进⾏⽐对,⾸先当前这条SELECT语句没有在事务300中进⾏查询,然后发现是落在⻩⾊区间,但是事务300没有在ReadView的未提交事务数组中,所以是可⻅的。即此时在第12时刻点,第四个事务所查找到的结果仍然是monkey301。
同时在第12时刻点,第五个事务执⾏了⼀条SELECT语句,想要查询⼀下当前id为1的数据。此时的版本链和ReadView如下
在这里插入图片描述
注意,此时第五个事务因为是该事务内的第⼀条SELECT语句,所以会重新⽣成在当前情况下的ReadView,即上图中所示的内容。可以看到,和第四个事务⽣成的ReadView并不⼀样,因为在之前的第10时刻点,事务100已经提交事务了。然后根据上⾯所说的⽐对规则,拿版本链中的第⼀个版本的事务ID为200进⾏⽐对,⾸先当前这条SELECT语句没有在事务200中进⾏查询,然后发现是落在⻩⾊区间,⽽且事务200是在ReadView的未提交事务数组中,所以是不可⻅的。此时通过回滚指针拿取下⼀个版本,发现事务ID仍然为200,经过分析后还是不可⻅的。此时⼜拿取下⼀个版本:事务ID为100进⾏⽐对,发现是在绿⾊区间,所以是可⻅的。即此时在第12时刻点,第五个事务所查找到的结果是monkey102(可以看到,即使是同⼀条SELECT语句,在不同的事务中,查询出来的结果也可能是不同的,究其原因就是因为ReadView的不同)。
在第13时刻点,事务200执⾏了commit操作,整段分析过程结束。
以上演示的就是MVCC的具体执⾏过程,在多个事务下,版本链和ReadView是如何配合进⾏查找的。上⾯还遗漏了⼀种情况没有进⾏说明,就是如果是DELETE语句的话,也会在版本链上将最新的数据插⼊⼀份,然后将事务ID赋值为当前进⾏删除操作的事务ID。但是同时会在该条记录的信息头(recordheader)⾥⾯的deleted_flag标记位置为true,以此来表示当前记录已经被删除。所以如果经过版本⽐对后发现找到的版本上的deleted_flag标记位为true的话,那么也不会返回,⽽是继续寻找下⼀个。
另外,如果当前事务执⾏rollback回滚的话,会把版本链中属于该事务的所有版本都删除掉。
自行分析
一共五个事务,且是可重复读
1时刻:所有事务开始,未产生版本链,但产生readview
2时刻:执行update语句,产生事务id100
3时刻:执行update语句,产生事务id200
4时刻:将id=1的事务名字更新为“monkey301”,同时生成事务id300
5时刻:将事务id为300的提交
6时刻:查找id=1的名字,查找不产生事务id,但产生readview,根据版本链的比对原则
【100,200】300
由于5时刻事务300已经提交所以和它进行比对,经过比对可以看出它不在未提交事务中,所以是可见的,查询结果为monkey301
7时刻:将id=1的事务名字更新为monkey101,版本链更新
8时刻:将id=1的事务名字更新为monkey102,版本链更新
9时刻:查询id=1的名字,由此表可以看出,事务100并未提交,根据可重复读,查询结果仍为monkey301,再看比对原则,查询并未在事务100中进行且事务100并未提交,再和下一版本链比对,发现此查询语句并未在事务300中进行,由此可以看出此事务属于未提交事务,所以是不可见的。
10时刻:事务100提交,与此同时,事务200将id=1的事务名字更新为monkey201,此时数组应该是这样200,300
11时刻:事务200将id=1的事务名字更新为monkey202
12时刻:两次查询id=1的名字,第一次:由此表可以看出在10时刻时事务100已经提交,随后事务200更行id=1数据的名字并未提交,根据可重复读,此查询沿用6时刻的readview,对已提交事务的更新在隔离级别下是不可见的,其他事务对该数据的查询仍然会返回提交前的版本,以保持事务之间的隔离性和一致性,所以查询结果为monkey301,再看比对原则,查询事务并未在事务300中进行,且未在事务200中进行,通过版本链比对,此事务属于未提交事务,所以不可见。
第二次:再次生成readview,此时的版本链应是200,300。根据可重复读事务隔离级别,事务100在第10时刻已经提交,根据比对原则,拿版本链中的第⼀个版本的事务
ID为200进⾏⽐对,⾸先当前这条SELECT语句没有在事务200中进⾏查询,然后发现是落在⻩⾊区间,⽽且事务200是在ReadView的未提交事务数组中,所以是不可⻅的。此时通过回滚指针拿取下⼀个版本,发现事务ID仍然为200,经过分析后还是不可⻅的。此时⼜拿取下⼀个版本:事务ID为100进⾏⽐对,发现是在绿⾊区间,所以是可⻅的。查询结果为monkey102.
13时刻:事务200进行commit操作。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值