今天和大家分享一下MVCC的知识,了解MVCC之后,你对mysql的事务隔离理解会更深入。
1.MVCC是什么?
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
它主要是用来处理mysql在多线程操作缓存数据时出现的一系列并发问题。
2.前置知识点
(1)什么是脏写、脏读、不可重复读、幻读?
网上相关资料很多,这里不做赘述
(2)四种隔离级别?
网上相关资料很多,这里不做赘述
(3)undo log版本链是什么?
在undo log日志里,每条数据除了自有的那些字段(表id、日志类型、数据页号等等),其实还会有两个隐藏字段,一个是trx_id,另一个是roll_pointer。这个trx_id就是最近一次更新的事务id,roll_pointer是指向你更新这个事务之前生成的undo log数据。
这里给大家举个例子:
假设有一个事务A,插入了一个数据A,此时的undo log数据结构如下:
因为事务id是10,所以这条数据的trx_id=10。因为是插入数据,所以没有下一个undo log数据,roll_pointer是空的。
接着,此时有一个事务B需要执行,事务B的id=20,那么执行完之后就会新生成一条undo log日志数据,trx_id=20,roll_pointer就会指向实际的回滚日志,也就是值A那条数据。结构如下图所示:
以此类推,在这个多个事务中,每个事务新生成的undo log日志数据的roll_pointer都会指向前一个undo log日志数据,一次行程undo log版本链。
3.基于undo log多版本链实现的ReadView机制
ReadView是什么?
简单来讲,ReadView就是执行一个事务时,会生成一个ReadView,这里面会有比较关键的4个字段:
1.m_ids:记录有哪些事务在mysql里还没有提交。
2.min_trx_id:m_ids里的最小值。
3.max_trx_id:下一个mysql要生成的事务id。
4.creator_trx_id:就是你这个事务本身的事务id。
下面我们举个例子,来说明下ReadView怎么用?
假设mysql里有个数据,很早之前就有事务插入了,事务id是20,如下图所示:
此时,有两个事务并发过来执行,分别是事务Aid=30,要读取这行数据。事务Bid=35,要修改这行数据。
此时事务A会做个判断,判断当前行的trx_id是否小于ReadView中的min_trx_id。此时发现30>20,所以可以得知在事务A开启之前,当前行的事务就已经提交了,因此shiwuA可以查到这条数据。如下图:
接着事务B开始操作,他把初始值修改成了值B,trx_id设置为自己的事务id,也就是35,同事roll_pointer指向了之前生成的undo log,然后事务B提交了。如下图:
这个时候,事务A再查询,就会发现一个问题,事务A就会发现trx_id变成了35,那么trx_id大于min_trx_id,同时小于ReadView里的max_trx_id=36。说明这个事务可能是和自己差不多时间开始的,然后就会看下这个trx_id是否在m_ids中,在m_ids中发现了35的id,那么就证明当前的数据是和自己同一时间并发启动的事务然后提交的,所以按道理这条数据不能让他看到,就把这条数据屏蔽掉,然后顺着roll_pointer找之前的undo log数据,然后就会找到trx_id=20的那条数据,小于min_trx_id,说明这条数据是在事务A提交之前就完成的,符合查询条件,就把这条数据给暴露出去。
通过undo log多版本链,加上ReadView进行判断的机制,就可以让你读取你应该读取哪个版本的值。
4.ReadView机制是如何实现读已提交隔离级别(RC隔离级别)的?
首先了解下RC隔离级别:是指你事务在运行期间,只要别的事务修改数据并且提交了,你就可以读取到修改之后的数据。(这种情况还是会发生不可重复读和幻读)
下面我就用画图的形式,来和大家一步一步的讲解实现过程:
首先假设有个数据,是事务id=50之前就插入进去的,现在活跃着两个事务,事务Aid=60,事务Bid=70。如下图:
现在事务B将初始值修改成了值B,此时trx_id=70,同时会生成一个undo log数据。如下图:
此时事务A发起查询操作,然后生成一个ReadView,m_ids=60、70,min_trx_id=60,max_trx_id=71,creator_trx_id=60。如下图:
此时事务A发现trx_id=70,大于min_trx_id,并且小于max_trx_id,说明当前事务是和事务A同时提交的,但是又因为trx_id=70,在m_ids里,说明当前事务还没有提交。那么根据读已提交隔离级别要求,事务未提交之前是不能查看修改值的,所以这里事务A看不到事务B的值B,只能根据roll_pointer指向找到上一条undo log数据,在做判断,发现trx_id=50,小于事务A,说明已经提交完成,所以事务A查到的数据是初始值。
接着,这里我们再将事务B提交,那么提交之后,事务A再进行查询,此时会发现m_ids已经变成了m_ids=60,那么说明事务B已经不再活跃m_ids数据中了,说明事务B已经提交了,因此事务A可以查看到事务B的值B。
这里需要注意一点,读已提交隔离级别中,事务每次执行,都会重新生成一个ReadView,因为只有这样才能获取到最新的事务id数据。
以上就是基于ReadView实现的RC隔离级别的原理。
5.ReadView机制是如何实现可重复读隔离级别(RR隔离级别)的?
我们已经了解了基于ReadView机制实现RC隔离级别的实现原理了,那么应该对RR隔离级别的实现原理也有一个大概猜想了,下面我们还是通过画图方式,来了解实现RR隔离级别的原理。
这里注意:RR隔离级别里,你这个事务 读取一条数据,无论读取多少次,都是一个值,ReadView也一样,别的事务哪怕事务提交了,也不能看到修改后的值,这样就避免了不可重复读的问题。
首先假设有个数据,是事务id=50之前就插入进去的,现在活跃着两个事务,事务Aid=60,事务Bid=70。如下图:
这个时候,事务A发起查询操作,这时候会生成一个ReadView,这是creator_trx_id=60,m_ids=60、71,min_trx_id=60,max_trx_id=71。如下图:
这个时候当前数据的trx_id=50,小于事务A的60,证明当前事务早在事务A之前提交了,所以事务A可以看到初始值。
接着就是事务B开始执行修改操作,此时trx_id=70,初始值改为值B,同时生成一个undo log,并且事务B提交了,也就是说此时事务B已经结束了。如下图:
那么此时事务A再次进行查询操作,大家说m_ids的值是多少呢?答案是m_ids=60,70。因为在RR隔离级别中,ReadView一旦生成,就不会改变,这个时候,虽然事务B已经提交了,但是事务A中的ReadView里,还是会有60、70两个活跃事务id。那么此时,事务A会判断trx_id是否大于60,很明显70>60,然后再看m_ids中是否有trx_id=70,是有的,所以这时候事务A还是认为事务B此时还是处于未提交状态,因此不会被允许查看事务B的值,他会根据roll_pointer找到上一条undo log数据,再次判断,50<60,满足条件,因此事务A查到的数据还是初始值。
大家看到这里是不是就感觉到了一下子就避免了不可重复读的问题呢。
同理,当有个事务C插入一条数据,事务id=80,然后提交,在事务A中查看到的max_trx_id还是71,这里会判读80>71,因此事务A会知道,当前事务C是在事务A发起之后才执行的,明显是不能查看他的数据的,因此这里也不会出现幻读的情况。这些都是依托ReadView机制实现的。