什么是MVCC?
MVCC(多版本并发控制),实现并发访问的一种方式,在mysql的Innodb引擎读已提交和可重复读两种隔离级别下,事务在select时实际上读取的是版本链中的数据。
MVCC只在Innodb引擎下,事务隔离级别为读已提交和可重复读时才存在
简单来理解,你可以想象一下Java中的写时复制(CopyOnWrite),其实思想都是差不多的,为了能够做到读写不互斥,所以在每次写入时,总是先copy一份,读请求还是读的原来的,而写请求时写的copy出来的,写完之后在用copy的覆盖原来的。
那么MVCC的实现思想也是一样的,通过生成多个版本的方式,来控制当前读请求已经读哪个版本,写请求又应该写哪个版本。
版本链
刚刚有提到了版本链,那么这个版本链又是什么呢?实际上就是多个版本之间的一种关联关系。
在InnoDB引擎表中,它的聚簇索引记录中有两个必要的隐藏列:
1、trx_id
这个id用来存储的每次对某条聚簇索引记录进行修改时候的事务id。
roll_pointer
每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)
比如现在有一个trx_id = 1,那么当修改时,则会在undo日志中新增一条记录,trx_id = 2,roll_pointer = (trx_id = 1的地址)
对某一行的age列进行更新操作,并且不提交事务,多个版本之间形成如下图的这种关系,就是版本链。
ReadView
刚刚我们说了MVCC只对读已提交和可重复读两种隔离级别下有意义,现在我们就来看看这两种隔离级别下分别是如下实现的。
我们先来回顾下,在读已提交和可重复读下产生的不同效果。
读已提交
会产生:不可重复读、幻读
不可重复读演示
A:set session transaction isolation level read committed;
A:start transaction;
A:select * from tran_test where id = 1; --假设结果为zhangsan
B:start transaction;
B:update tran_test set name = 'zs' where id = 1; --把id为1的name修改为zs
A:select * from tran_test where id = 1; -- B未提交所以A此时查询结果还是zhangsan
B:commit;
A:select * from tran_test where id = 1; -- B事务提交后,A再查询此时结果已经变成了zs
可重复读
只会产生幻读,不会产生不可重复读
幻读演示
A:set session transaction isolation level read committed;
A:start transaction;
A:select * from tran_test where id = 4; --结果为空
B:start transaction;
B:insert into tran_test values(4,'zhaoliu'); -- B插入一条id=4的数据
A:select * from tran_test where id = 4; --结果依旧为空
B:commit; -- B提交事务
A:select * from tran_test where id = 4; --结果依旧为空
A:insert into tran_test values(4,'zhaoliu'); -- 插入失败,报主键冲突
实际上对于不可重复读的问题就是通过ReadView来解决的。
解决不可重复读的方式
ReadView会先通过一个链表来记录当前已开启但还未提交的trx_id(假设我们叫这种事务为:当前活动事务)。
举例说明:
当前trx_id = 3,修改age=2,此时事务还未提交,版本链如下:
当前ReadView记录了trx_id = 3的事务,此时另一个事务执行select查询语句,于是从版本链中寻找,第一条trx_id = 3,已经在ReadView中,所以不能访问,于是找下一条,trx_id = 2,小于ReadView记录中的最小值,所以可以访问,于是返回age = 2的记录。
现在假设刚才trx_id = 3提交了,然后又新建了一个事务,修改age = 4,trx_id = 4,并且还未提交,版本链如下:
ReadView通过一个链表来记录当前已开启但还未提交的trx_id(当前活动事务)。
举例说明,在读已提交的场景下:
当前trx_id = 3,修改age=2,此时事务还未提交,版本链如下:
当前ReadView记录了trx_id = 3的事务,此时另一个事务执行select查询语句,于是从版本链中寻找,第一条trx_id = 3,已经在ReadView中,所以不能访问,于是找下一条,trx_id = 2,小于ReadView记录中的最小值,所以可以访问,于是返回age = 2的记录。
现在假设刚才trx_id = 3提交了,然后又新建了一个事务,修改age = 4,trx_id = 4,并且还未提交,版本链如下:
现在关键部分来了,看看读已提交和可重复读两种不同隔离级别的做法。
读已提交
当另一个事务再次select时,会重新生成一次ReadView,由于当前活动事务id为4,所以ReadView中记录的trx_id = 4,然后再根据之前的方式,最终会查询到age = 3的记录。
可重复读
当另一个事务再次select时,并不会重新生成ReadView,而是继续使用上一次的ReadView,那么ReadView中记录的当前活动事务id就还是为3,那么最终还是查询到age = 2的记录。
所以说在读已提交的隔离级别下,每次select时都会重新生成一个ReadView,而在可重复读时,只会在第一次select时生成ReadView,之后事务中的每次select,都只会复用第一次生成的ReadView。
总结
MVCC实际上就是为了能够让读写不互斥的一种解决方式,就如同Java中的CopyOnWrite,linux中的fork函数,Redis的RDB,后3者也都是写时复制的思想,MVCC也就是做了同样的事情,只不过具体的实现方式有点区别。