全称Multi-Version Concurrency Control,即多版本并发控制,主要是为了提高数据库的并发性能。
1、版本链
对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列:
1、trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。
2、roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
为了实现事务的原子性,InnoDB存储引擎在实际进行增、删、改一条记录时,都需要先把对应的undo日志记下来。
事务id为10的事务,插入一条数据
事务30和事务50分别对这条数据进行修改操作
每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表:
对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id。于是可以利用这个记录的版本链来控制并发事务访问相同记录的行为,那么这种机制就被称之为多版本并发控制(Mulit-Version Concurrency Control MVCC)。
2、ReadView
ReadView中主要包含4个比较重要的内容:
m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。注意max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。
creator_trx_id:表示生成该ReadView的事务的事务id。
2.1、使用读已提交解决脏读
此隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。在MuSQL中读已提交和可重复的很大一个区别就是ReadView的生成时机不同。
假设现在表 table 中只有一条由事务id为10的事务插入的一条记录。
读已提交 —— 每次读取数据前都生成一个ReadView:
现在系统里有两个事务id分别为80、120的事务在执行。
第1次select的时间点
执行过程
在执行SELECT语句时会先生成一个ReadView:
ReadView的m_ids列表的内容就是[80, 120],min_trx_id为80,max_trx_id为121。
然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列data的内容是 '222',该版本的trx_id值为80,在m_ids列表内,所以不符合可见性要求(trx_id属性值在ReadView的min_trx_id和max_trx_id之间说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问),根据roll_pointer跳到下一个版本。
下一个版本的列data的内容是'111',该版本的trx_id值也为80,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。下一个版本的列data的内容是'xx',该版本的trx_id值为10,小于ReadView中的min_trx_id值,所以这个版本是符合要求的,最后返回给用户的版本就是这条列data为'xx'的记录。
所以有了这种机制,就不会发生脏读问题!因为会去判断活跃版本,必须是不在活跃版本的才能用,不可能读到没有commit的记录。
不可重复读问题
然后,我们把事务id为80的事务提交一下,然后再到事务id为120的事务中更新一下表table中data的记录。
最后版本链
第二次select
执行过程:
在执行SELECT语句时会又会单独生成一个ReadView,该ReadView信息如下:
m_ids列表的内容就是[120](事务id为80的那个事务已经提交了,所以再次生成快照时就没有它了),min_trx_id为120,max_trx_id为121。
然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列data的内容是'bbb',该版本的trx_id值为120,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
下一个版本的列data的内容是'aaa',该版本的trx_id值为120,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
下一个版本的列name的内容是'222',该版本的trx_id值为80,小于ReadView中的min_trx_id值120,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为'222'的记录。
2.2、使用可重复读级别解决不可重复读问题
在第一次读取数据时生成一个ReadView
对于使用REPEATABLE READ隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。
在上面的例子中:
这个SELECE1的执行过程如下:
在执行SELECT语句时会先生成一个ReadView:
ReadView的m_ids列表的内容就是[80, 120],min_trx_id为80,max_trx_id为121。
然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列data的内容是'222',该版本的trx_id值为80,在m_ids列表内,所以不符合可见性要求(trx_id属性值在ReadView的min_trx_id和max_trx_id之间说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问),根据roll_pointer跳到下一个版本。
下一个版本的列data的内容是'111',该版本的trx_id值也为80,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
下一个版本的列data的内容是'xx',该版本的trx_id值为60,小于ReadView中的min_trx_id值,所以这个版本是符合要求的,最后返回给用户的版本就是这条列data为'xx'的记录。
之后,我们把事务id为80的事务提交一下,然后再到事务id为120的事务中更新一下表table中data的记录。
这个SELECE2的执行过程如下:
因为当前事务的隔离级别为可重复读,而之前在执行SELECE1时已经生成过ReadView了,所以此时直接复用之前的ReadView,之前的ReadView的m_ids列表的内容就是[80, 120],min_trx_id为80,max_trx_id为121。
根据前面的分析,返回的值还是'xx'。
也就是说两次SELECT查询得到的结果是重复的,记录的列data值都是'xx',这就是可重复读的含义。
2.3、MVCC下的幻读现象和解决
表中只有一条事务10插入的数据
第一次select
在执行SELECT语句时会先生成一个ReadView:
m_ids列表的内容就是[80],min_trx_id为80,max_trx_id为81。
然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列data的内容是'xx',版本的trx_id值为10,可以被当前事务访问,最后返回给用户的版本就是这条列data为'xx'的记录。
第二次select
因为当前事务的隔离级别为可重复读,而之前在执行SELECE1时已经生成过ReadView了,所以此时直接复用之前的ReadView, m_ids列表的内容就是[80],min_trx_id为80,max_trx_id为81。
然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列data的内容是'222',版本的trx_id值为120,如果被访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
最终,最后返回给用户的版本还是这条列data为'xx'的记录。这是这种情况下,不会出现幻读现象。
特殊幻读现象:
此时第一次读是 没有数据,第二次 出现一条数据
就是第一次读如果是空的情况,且在自己事务中进行了该条数据的修改。
2.4、ReadView中的比较规则
1、如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
2、如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
3、如果被访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
4、如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间(min_trx_id < trx_id < max_trx_id),那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
2.5、总结
MVCC(Multi-Version Concurrency Control ,多版本并发控制)进行普通的SEELCT查询时才生效。
它指的就是在使用读已提交、可重复读这两种隔离级别的事务在执行普通的SELECT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。
读已提交、可重复读这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,读已提交在每一次进行普通SELECT操作前都会生成一个ReadView,而可重复读只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了,从而基本上可以避免幻读现象(就是第一次读如果ReadView是空的情况中的某些情况则避免不了)。