MVCC实现原理是一道非常高频的面试题,底层的原理还是比较难以理解,今天我查看了一些文章,试着做一个简单的梳理,资料全部来自于互联网,我只是做个简单的收录和整理,以便用时翻阅。 --------------------生活纵有千般苦,莫要虚度一世人
目录
5.MVCC是否解决了幻读问题呢?或者在InnoDB下是如何解决幻读问题的?
1.先来看看这张图引出MVCC:
对于图上的数据库事务,事务的特性,事务并发存在的问题,隔离级别这些比较基础的概念就不说了。
直接抛出我们的疑问 数据库是如何保证事务的隔离性的?
答:数据库是通过加锁,来实现事务的隔离性的。加锁确实好使,可以保证隔离性。比如串行化隔离级别就是加锁实现的。但是频繁的加锁,导致读数据时,没办法修改,修改数据时,没办法读取,大大降低了数据库性能。那么,如何解决加锁后的性能问题的?
答案就是,MVCC多版本并发控制!它实现读取数据不用加锁,可以让读取数据同时修改。修改数据时同时可读取。
关于MySQL锁的文章这里就不展开了,感兴趣的可以看我的另外一篇的博客CSDN
2.MVCC中的 概念:
隐式字段
对于InnoDB存储引擎,在数据库的每一条数据其实都有两个隐藏的字段,事务id(trx_id)和回滚指针(roll_pointer),如果表中没有主键和非NULL唯一键时,则还会有第三个隐藏的主键列row_id。
在数据库里面大概就是这个样子:
比如我们插入这个条记录的时候,就会把当前这条事务的id插入到这条数据的隐藏字段。
事务版本号
事务每次开启前,都会从数据库获得一个自增长的事务ID,可以从事务ID判断事务的执行先后顺序。这就是事务版本号。这里要注意啊 普通的select语句比如select * from user 这种语句是没有事务ID生成的哈,只有insert,update这些才会为当前的session生成事务Id的.
undo log
undo log是回滚的日志,可以简单的理解为记录了MySQL的一些操作,当发生事务回滚的时候根据这些记录去回滚数据。
-
事务回滚时,保证原子性和一致性。
-
用于MVCC快照读。
版本链
多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer),连成一个链表,这个链表就称为版本链
ReadView
它就是事务执行SQL语句时,产生的读视图。实际上在innodb中,每个SQL语句执行前都会得到一个Read View。这里要注意了,在一次的session中,第一次select的时候,就会生成这个一致性的视图,他是由查询的时候所有未提交的事务id的数组,和已创建的(包含已提交和未提交)最大的事务id组成,查询的数据结ReadView 做比对,从而得到快照的结果。隔离级别不一样,ReadView 的工作方式就不一样,可以这样说,在可重复读与读已提交的这两个隔离级别下,他们的实现就是ReadView 的生成不一样。
匹配规则:
3.MVCC的流程演示:
下面我们来讲一个实际的案例,通过实际的案例来套这个MVCC的原理
首先我们假设数据库中存在了这样的一条数据 ,
假设我开启了第一个session, 并且执行了一条update语句,操作的表不一定是我们的account表,执行update语句是为了生成一个事务的id,假设生成的事务id就是100,接着开启第二个session,执行update语句,假设生成的事务id是200,
紧接着开启第三个session,这个是跟新了数据库的account表,生成的事务id为300,并且执行了提交的操作. update account set name='lilei03' where id=1, 此时数据库的数据会变成这个样子,会插入一条新的记录,同时把之前的那条记录放到undo日志里面去,同时新纪录的回滚指针会指向旧数据
接着execl上的顺序,我再开启一个session,执行一条select语句,select name from account where id=1 凭借我们的经验可知,查询的结果会是lilei03,下面我们在MySQL的默认隔离级别下,也就是可重复读的隔离级别下,用MVCC的角度去看这个select 的执行流程.
好,一步一步来,首先我们执行select name from account where id=1 ,会为当前的session生成一致性的视图readview,根据定义可知,所有未提交的事务id的数组为[100,200],再加上已创建的(包含已提交和未提交)最大的事务id,为300,所以最终这个readview为[100,200],300
然后他匹配数据是从最新的那一条数据开始匹配,我们最新的一条数据的事务id是300,300落在了黄色的区间,落在黄色的区间后分为了两种情况,此时这个事务id是不在数组[100,200]中的,所以得到的结论是表示这个版本是已提交的事务生成的,所以可见返回。查询的结果为lilei03。
现在我们继续跟着execl里面的命令执行。
我们在第一个session里面继续执行了两条跟新操作。
update account set name='lilei01' where id=1
update account set name='lilei02' where id=1
没有体交,此时版本链变成了这样:
这个时候我们再在session4里面执行select语句,select name from account where id=1 ,此时由于这个session中之前执行过一条select语句已经生成了一致性的视图ReadView ,更重要的是由于隔离级别是可重复读,所以此时这个ReadView 不会新计算生成而是直接复制一份最开始生成的,所以此时ReadView 的值依然是[100,200],300。
根据版本的匹配规则,我们从最新的记录开始找,最新的事务id为100,落在黄色区域中,并且在数据[100,200]中,表示他不可见,继续找,直到找到事务id为300,的才可见,所以此次查询的结果集为name=lilei03,根据经验这个结果也是正确的。
下面跟着execl里面的顺序 我们把session1里面的事务提交,然后再在session2里面执行两条update语句,
update account set name='lilei03' where id=1
update account set name='lilei04' where id=1
此时的版本链就变成了这个样子
我们在session4里面再次执行查询语句 select name from account where id=1,由于隔离级别还是可重复读,所以ReadView 依然是[100,200],300。根据匹配规则,查询的结果集还是name=lilei03
假设这里的事务隔离级别读已提交,那么每次select就会生成新的ReadView ,此时生成的ReadView 就是[200],300,
根据版本的匹配规则,会挑出这一条数据,所以在读已提交的隔离级别下此时查询的结果集为lilei02
通过上面的例子相信你已经掌握了MVCC的底层机制,下面来总结一下他的大致流程:
我们总结一下大致的流程就是:
-
获取事务自己的版本号,即事务ID
-
获取Read View
-
查询得到的数据,然后Read View中的事务版本号进行比较。
-
如果不符合Read View的可见性规则, 即就需要Undo log中历史快照;
-
最后返回符合规则的数据
InnoDB 实现MVCC,是通过Read View+ Undo Log
实现的,Undo Log 保存了历史快照,Read View可见性规则帮助判断当前版本的数据是否可见。
不同隔离级别下,Read View的工作方式不同
4.快照读和当前读
我们读取的Undo Log 里面的数据都叫快照读,那么如何才能当前读最新的呢?
显式加锁的都是当前读 例如:
select * from core_user where id > 2 for update;
select * from account where id>2 lock in share mode;
5.MVCC是否解决了幻读问题呢?或者在InnoDB下是如何解决幻读问题的?
从理论上讲,RR隔离级别下是存在虚幻读问题的,但是实际测试中呢,MySQL在InnoDB的存储引擎下是没有幻读问题的。
幻读是指在同一个事物中,前后两次查询相同范围的时候,得到的结果不一致,
我们要知道在InnoDB中RR隔离级别下已经解决了幻读的问题了。InnoDB里面引入了间隙锁和next-key lock机制去解决幻读问题,
假设现在存在这样的一个B+tree的索引结构,
这个结构里面存在1,4,7,10四个索引元素,
间隙锁是锁定一段范围内的索引记录,其他事物对这个区间的插入跟新删除都会被阻塞。
但是像这种,需要锁定多个索引区间,InnoDB引入了一个叫next-key lock机制,next-key lock相当于间隙锁和记录锁的合集。记录锁锁定存在记录的行也就是行锁,比如for update产生的锁就是行锁,间隙锁锁定的是记录行之间的间隙.
每个数据行上的非唯一索引列都存在一个都会存在一把next-key lock,像这样,当我们事物持有这样一行数据的next-key lock的时候,会锁住一段左开右闭区间的数据。,所以id>4 for update的时候会加一把next-key lock锁,锁定的区间就是(4,7],(7,10],(10,正无穷].
来源: