认识MVCC
MVCC 是英文Multi-Version Concurrency Control 多版本并发控制的首字母简拼。在上文MYSQL事务隔离级别中,我们已经知道,在可重复读的级别下,不管其他事务怎么修改一条数据,一个事务内查询同一条语句,不管查询几遍查到的数据都是一样的。那他是怎么做到的,是加锁吗,加锁性能也太低了,mysql肯定不会这么做。他就是依靠mvcc机制来实现这样的效果。但是在可串行化级别下,mysql就是通过加锁互斥来保证隔离性,频繁的加锁互斥所以可串行化级别效率也是最低的。所以一般不用这个级别。所以,mvcc简单点理解就是不使用加锁,而是使用多版本控制来达到事务之间的隔离性,这样显然效率会比较高。
在mysql中除了可重复读用到mvcc机制之外,还有读已提交级别也用到mvcc机制。下面的介绍中默认是基于可重复读级别来介绍,读已提交级别也是大同小异在下文会分析他们的不同之处。
认识undo日志版本链
MVCC是多版本并发控制,那多版本是怎么做到的呢?多版本就是通过undo日志版本链来实现的。具体的说就是一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且会有两个隐藏字段一个是记录每个事物的事务ID(trx_id)一个是回滚指针(roll_pointer)指向上一个事务。undo日志版本链是对所有事务可见的,一行数据不管被哪几个事务修改,修改几次都会维护到undo日志中,并在undo日志中维护一个链条,目的是用于事务回滚(当前事务失败回滚到指针指向的上一个事务)或事务隔离(每个事务读到的版本有所不同)。
如上图,假设有一张账号表account,id=1的记录name初始值是lilei。
1. 被事务id为300的事务,修改成name=lilei300,指针指向事务80的记录
2. 又被事务为100的事务,修改成name=lilei1,指针指向事务300的记录
3. 事务为100的事务又进行修改成name=lilei2,指针指向事务100的记录
4. 再被事务为200的事务修改成name=lilei3,指针指向事务为100的记录
5. 最后还是在事务200中修改成name=lilei4,指针又指向上一条记录。
综上,每次对id=1的修改,记录下来并用指针指向上一次记录,形成了一个undo日志版本链。
认识read-view
read-view是一致性视图,如果说undo日志版本链是mvcc中的多版本体现,那read-view就是并发控制中必不可少的一环,因为每一个事务都会有他自己的readview。在可重复读隔离级别下,当事务开启,执行任何查询select语句的时候会生成当前事务的一致性视图read-view,该视图在事务结束之前都不会变化。这个read-view由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已提交的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的结果。read-view是每个事务自己独有的而且是一成不变的,而undo日志版本链是所有事务共有一份的。他的结构类似这样
read-view[未提交事务id1,未提交事务id2....未提交事务idN],已提交最大事务idM
他的结构有两部分组成数组中是所有未提交的事务集合,数组外面和数组并列的是已提交的最大事务ID。
但是呢,在读已提交的级别下,read-view在事务中并不是一成不变的,而是每次查询都会生成一个新的read-view,这也是可重复读和读已提交的区别。
MVCC工作原理
上面分别介绍了undo日志版本链和read-view,他们之间的交互和关系就是MVCC的工作原理。一个事务的查询,是从undo日志版本中最新的版本开始比对的,select语句产生后,readview就固定下来了。此时readview是已知的,undo版本日志链也是已知的,具体如下:
1. 如果当前比对的undo日志版本 trx_id小于未提交的事务ID也就是小于readivew数组中最小的事务ID,表示这个版本是已提交的事务生成的,这个数据是可见的;就可以返回给客户端。
2.如果当前比对的undo日志版本版本trx_id刚好等于最大已提交的事务,说明是已提交的可直接返回给客户端。
3. 如果当前比对undo日志版本 trx_id大于最大提交的事务ID也就是readview中最大事务ID,表示这个版本是由未开始的事务生成的,是不可见的,那就要比对undo日志版本中的上一个版本。
4.如果当前比对的undo日志版本trx_id 是未提交的事务ID也就是在readview的数组中那说明这个版本是由还没提交的事务生成的,是不可见的,继续去比较上一个undo日志版本记录。
MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。
案例分析
以上是通过理论层面认识mvcc机制,现在通过一个具体的案例加深对mvcc的理解。
假设account表有一条id=1,name=lilei的初始记录。
这里补充一点:begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作InnoDB表的语句,事务才真正启动,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。执行到第一条select语句的时候才会生成read-view。
案例1
假设当前数据库中有这样4个事务,事务100更新test表未提交,事务200更新test表未提交,事务300更新account表已提交,第一条select语句在事务300提交之后查询account表ID=1的记录。此时select 1中的第一条查询语句会产生readview,根据readview的生成规则,是由所有未提交的事务和已提交的最大事务组成的,所以他是这样的:
readview[100,200],300
100,200是当前未提交的事务,300是已提交的最大事务
这里面隐藏了两个信息:
1.事务id小于100的说明都是提交的事务
2.事务id大于300的都是未开始的事务
此时account=1的undo版本日志链是这样的:
先比较undo版本日志链中第1条记录,事务id=300,不在readview[100,200]数组中,在比对readview中最大事务ID(300)刚好相等,就返回这条版本记录name=lilei300,所以,selec 1中读的name=lilei300。
案例2
案例2在案例1的基础上进行,事务100,对account表进行两次更新但是未提交,select1,在事务100更新之后,又做了一次一模一样的查询。此时select语句的readview是多少呢?因为这是可重复读级别所以此时readview还是readview[100,200],300。而此时undo日志版本链是怎样的呢?如下图:
因为事务100对account表id=1记录做了两次修改,所有版本链多了两条记录,并且都指向上一个记录。还是老样子,从第1条记录开始比对,事务id=100,100跟readview[100,200],300相比是在数组中,是未提交的,在比对上一条记录,事务id还是等100跟readview[100,200],300相比还是在数组中,是未提交的在比对上一条记录,事务id=300跟readview[100,200],300不在数组中,在跟300对刚好相等,所以返回的还是lilei300,这就达到了可重复读的效果。
案例3
案例3还是基于前面两个案例的基础上,事务100此时对前面两个修改进行提交,事务200更新了两次,但是都没有提交。select 1中 又做了一次一模一样的查询,select2中另一个事务也做了一次一模一样的查询。那此时结果会是什么样子的呢?
先看此时的unodo日志版本链,因为undo日志版本链是对所有事务可见的:
先看select1中的第三条查询语句,跟之前一样,前面说过可重复读级别情况下readview是在第一条查询语句的时候就锁定了,所以readview还是readview[100,200],300。
此时先拿出undo日志版本链的第一条记录事务id=200,跟readview[100,200],300相比在数组中,是未提交的所以继续比对上一条记录,事务id还是等200,继续比对上一条事务id=100跟readview[100,200],300相比还是在数组中还是未提交的(尽管事务100此时提交了但是对select 1事务来讲事务100是未提交的,因为select 1 第一条查询语句开始的时候他是未提交的,select 1整个事务中readview是由第一次查询的时候决定了readview)所以继续比对上一条记录事务ID还是100,继续比对上一条事务ID=300,刚好是已提交的所以就返回,select1中的第三条语句读到的还是lilei300。即使事务100提交了,select 1中多次读到的数据都是lilei300有没有发现,这就是可重复读。
在看select2,select2中是第一次查询,此时锁定readview,readview的规则前面说过,是由当前所有未提交的事务和最大已提交事务组成,由于此时事务100已经提交了,所以,select2中的readview为:readview:[200], 300 。也是按undo日志版本链一次比对,先比对第一条,事务id=200和readview:[200], 300比较发现是在数组中是未提交的事务,继续比对上一条记录事务id还是200。继续比对上一条记录事务ID=100,诶,不在readview:[200], 300数组中,而且是小于200的说明是已提交的事务,此时就锁定这条记录,所以select2中查到的name=lilei2。
有没有发现,不管其他事务怎么改变值,select1中读到的都是lilei300,而且,select2中读到的是lilei2。这样就达到事务之间的隔离性和可重复读。
最后提一句,在读提交级别中,所有流程都是一样的,只是readview的生成时机有所区别,可重复读是在第一条查询语句开始的时候就锁定了readview,所以每一次读到的值都一样。而读提交级别的时候是每一次查询的时候都会重新生成readview,所以读提交每一次读到的都是最新值。
以上就是mysql中mvcc机制的大致原理,主要是利用undo日志版本链和readview两个机制保质事务的隔离性。