MySQL 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指针。

        (注意,添加的隐藏字段并不是很多人认为的创建时间和删除时间,同时在MySQL中MVCC的实现也不是通过什么快照来实现的。之所以有这种说法可能是源自于《高性能MySQL》一书中对MySQL中MVCC的错误结论,然后就人云亦云传开了(注意,我这里一直强调的是MySQL中MVCC的实现,是因为在不同的数据库中可能会有不同的实现)。所以说看源码和看官方文档才是最权威的解释)


2 ReadView

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


3 版本链

        所有版本的数据都只会存一份,然后通过回滚指针连接起来,之后就是通过一定的规则找到具体是哪个版本上的数据就行了。假设现在有一张account表,其中有id和name两个字段,那么版本链的示意图如下:

        而具体版本链的比对规则如下,首先从版本链中拿出最上面第一个版本的事务ID开始逐个往下进行比对:

        (其中min_id指向ReadView中未提交事务数组中的最小事务ID,而max_id指向ReadView中的已经创建的最大事务ID)

  • 如果落在绿色区间(DB_TRX_ID < min_id):这个版本比min_id还小(事务ID是从小往大顺序生成的),说明这个版本在SELECT之前就已经提交了,所以这个数据是可见的。或者(这里是短路或,前面条件不满足才会判断后面这个条件)这个版本的事务本身就是当前SELECT语句所在事务的话,也是一样可见的
  • 如果落在红色区间(DB_TRX_ID > max_id):表示这个版本是由将来启动的事务来生成的,当前还未开始,那么是不可见的;
  • 如果落在黄色区间(min_id <= DB_TRX_ID <= max_id):这个时候就需要再判断两种情况:
    • 如果这个版本的事务ID在ReadView的未提交事务数组中,表示这个版本是由还未提交的事务生成的,那么就是不可见的;
    • 如果这个版本的事务ID不在ReadView的未提交事务数组中,表示这个版本是已经提交了的事务生成的,那么是可见的。

        如果在上述的判断中发现当前版本是不可见的,那么就继续从版本链中通过回滚指针拿取下一个版本来进行上述的判断。


4 演示过程

        下面通过一个示例来具体演示MVCC的执行过程(假设是在可重复读事务级别下),当前account表中已经有了一条初始数据(id=1,name=monkey):

Transaction 100Transaction 200Transaction 300无事务ID无事务ID
1begin;begin;begin;begin;begin;
2UPDATE test SET a='1' WHERE id = 1; 
3UPDATE test SET a='2' WHERE id = 2; 
4UPDATE account SET name = 'monkey301' WHERE id = 1;
5commit;
6SELECT name FROM account WHERE id = 1;
7UPDATE account SET name = 'monkey101' WHERE id = 1;
8UPDATE account SET name = 'monkey102' WHERE id = 1;
9SELECT name FROM account WHERE id = 1;
10commit;UPDATE account SET name = 'monkey201' WHERE id = 1;
11UPDATE account SET name = 'monkey202' WHERE id = 1;
12SELECT name FROM account WHERE id = 1;SELECT name FROM account WHERE id = 1;
13commit;

        从左往右分别是五个事务,从上到下是时刻点。其中在第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。但是同时会在该条记录的信息头(record header)里面的deleted_flag标记位置为true,以此来表示当前记录已经被删除。所以如果经过版本比对后发现找到的版本上的deleted_flag标记位为true的话,那么也不会返回,而是继续寻找下一个。

        另外,如果当前事务执行rollback回滚的话,会把版本链中属于该事务的所有版本都删除掉。

  • 57
    点赞
  • 282
    收藏
    觉得还不错? 一键收藏
  • 29
    评论
### 回答1: MySQLMVCC(Multi-Version Concurrency Control)机制是通过为每个读操作创建一个版本(Version)并保留旧版本来实现的。这个机制允许多个事务同时访问同一数据行,同时确保它们不会互相干扰或产生冲突。 MVCC在MySQL中的实现方式是,对于每一行数据,在表中存储一个隐藏的系统版本号(system versioning),并将每个操作(包括SELECT查询)的时间戳与该行的版本号进行比较。当读取一行数据时,MySQL会根据当前的事务时间戳和行的版本号来决定该行是否可见。如果行的版本号早于当前事务的时间戳,则说明该行是旧版本,不可见;如果行的版本号晚于当前事务的时间戳,则说明该行是新版本,可见。 在MVCC机制下,读操作不会阻塞写操作,写操作也不会阻塞读操作。因此,MVCC机制可以提高并发性能和可伸缩性,使得多个事务可以同时访问同一数据库而不会产生锁定和阻塞问题。 但是,MVCC机制也有一些限制。例如,如果事务A在读取某个数据行的同时,事务B修改了该行的值,那么事务A在提交时就会检测到该数据行已经被修改,从而回滚该操作。此外,MVCC机制也会占用更多的存储空间来存储旧版本的数据行。 ### 回答2: MySQLMVCC(多版本并发控制)是一种用于处理并发访问的机制。MVCC是通过在数据库的各种操作(如事务的开启、读取和写入)中使用隐藏的时间戳来实现的。 MVCC的主要目标是避免读取和写入操作之间的冲突,从而提高数据库的并发性能和资源利用率。它通过在内部为每个事务提供一个唯一的时间戳来实现。每个事务在开始时都会获得一个时间戳,并且事务中的每个操作都使用这个时间戳。 当一个事务读取数据时,它只能读取它开始时间之前的数据版本。这样可以避免读取到其他事务正在写入或修改的数据,从而保证读取操作的一致性和隔离性。 当一个事务写入数据时,它会创建一个新的数据版本,并将其与事务的时间戳关联。这个新版本的数据不会立即覆盖旧的数据,而是以一种类似于快照的方式存在。其他事务在读取数据时仍然可以访问旧版本的数据。 MVCC还使用了回滚段(undo log)来处理事务的回滚操作。当一个事务被回滚时,数据库会使用回滚段将所有该事务做出的修改逆转回去,从而恢复到事务开始之前的状态。 需要注意的是,MVCC机制对于并发性能和资源利用率的提升是有限的。在高并发的情况下,数据库可能会出现锁等待和资源竞争的问题。为了进一步优化并发性能,可以考虑使用其他技术,如乐观并发控制(Optimistic Concurrency Control)和分布式数据库。 ### 回答3: MySQLMVCC(Multi-Version Concurrency Control)机制是一种并发控制技术,用于处理数据库中的读写冲突。它允许多个事务同时读取数据库,同时也使得读写冲突被有效地解决。 MVCC机制基于以下两个重要的概念:版本号和快照。 首先,每个表中的每个行都有一个版本号。当一个事务对某行进行修改时,会为该事务创建一个新的版本,并将旧版本标记为过期。这样,读取该行的事务会读取到未过期的版本,而不会受到写用户的影响。同时,这也避免了仅读用户被阻塞的情况。 其次,为了实现读取未过期版本的行,MVCC机制通过创建快照来实现。快照是数据库在某个时间点的一个镜像,其中包含了未过期的行版本。当一个读取事务开始时,会生成一个当前的数据库快照,并基于这个快照来读取数据行。这样,读取事务不会看到在其开始时(即快照生成时)已提交的写入事务,从而实现了读写并发。 MVCC机制对于提高数据库的并发性能非常重要。它允许多个事务同时进行读操作,提高了数据库的并发处理能力。此外,它也避免了读写冲突和阻塞的情况,提高了数据库的效率和稳定性。 总之,MySQLMVCC机制通过使用版本号和快照来实现读写并发控制和冲突的解决。它是提高数据库并发性能和减少阻塞的关键技术之一,并且在实际的数据库应用中扮演着非常重要的角色。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 29
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值