MVCC介绍
MVCC其实就是多版本并发控制,用来解决幻读问题,同时解决幻读问题还有间隙锁。
UNDO LOG
undo log是用来事务回滚的,具体细节回看undo log文章
这里要注意的一点是,每条undo log就是一个表的版本
undo log又分为两种
- Insert undo log:针对Insert操作的
- Update undo log:针对update和delete操作的
update操作又分为两种,一种是更新主键,一种是不更新主键
- 更新主键:先新增后标记删除旧记录(产生Insert undo log和update undo log)
- 不更新主键:单纯产生一个新版本的update undo log
MVCC原理
每个事务执行一条更新操作语句(DML语句)才会生成自己的事务Id
而每个事务执行一条查询语句就会获得一个一致性视图read-view,以后的查询都会基于这个去查询
read-view
read-view是由所有未提交事务id数组和已提交最大事务id所组成
[100,200,300] 400 //数组里面的是未提交事务id,旁边的是已提交最大事务id
那么根据read-view就可以将所有事务划分为3部分
- 已提交
- 未提交与已提交
- 未提交
那么这三部分是如何区分的呢?
我们将read-view维护的未提交数组称为活跃区,在这个活跃区里面,最小的事务id称为min_id,最大的称为max_id
在活跃区以外的事务id,要么是未开始,要么是已经提交的。由于事务id是有序创建的,所以小于min_id的事务一定是已经提交的,所以已提交的区域就是小于min_id的,然后,未提交与已提交的区域就是由read-view维护的活跃区和最大已提交的事务id组成(活跃区为未提交,max_id到已提交最大事务id的区间一定是已提交【道理与小于min_id一样】),大于最大已提交的事务id肯定都是未开始的,所以归为未提交区域。
这里再对max_id到最大已提交事务id区间进行说明,要分两种情况讨论
- max_id小于最大已提交事务id,那么max_id到最大已提交事务id区间内的事务一定是已提交的
- max_id大于最大已提交事务id,那么区间里面,不一定都是已提交的,区间里面的事务id如果出现在活跃区,就是未提交的,如果不在活跃区出现就是已提交的
版本链
版本链是由undo log形成的,undo log是一个表数据里面的一个历史版本(每条行记录都有自己的版本链),并发的事务可能会由于查询同一条行记录,那么获取的版本链是一样的(全局使用),当事务进行DML操作时,对应的行就会生成对应的undo log(新的版本),在undo log里面标上自己的事务id,让后使用指针(row_pointer)来关联上之前的undo log,那么undo log就会形成一个链表,称为版本链(新版本的会在链表前面,每条行记录会有两个隐藏字段,一个是trx_id,也就是事务id,另一个是row_pointer指向上一个undo log版本)
实现
现在事务开始进行查数据(Select),根据read-view维护的活跃区和最大已提交事务id
查数据的时候,就是根据undo log的版本链进行匹配,匹配的规则(再提一下,每个undo log就是一个行记录的版本,所以查的行数据其实就是一个undo log)
- 事务id小于min_id是已提交的,可见,直接返回undo log
- 事务id在活跃区的,不可见,通过链表找下一条undo log
- 事务id大于max_id,同理按照前面所提到的规则去判断是否已提交,提交就是可见,直接返回undo log,未提交就是不可见,通过链表找下一条undo log
- 事务id大于已提交最大事务id:是不可见的,通过链表找下一条undo log
经过这样去匹配,就可以找到对应自己版本的undo log
注意,上述的规则对于可重复读和读已提交都是一样的
可重复读与读已提交的区别
- 可重复读:每次读的快照都是同一个undo log
- 读已提交:每次读的快照都是最新的undo log
这两者底层实现的区别,其实就是read-view的维护,可重复读,一旦执行了SELECT语句之后,read-view不会再改变,但读已提交,每次的SELECT都会去更新read-view
补充:Insert undo log
Insert update log对于当前事务来说必定是可见的,因为是自己操作的,但对于其他事务来说,必定是不可见的,因为还没有事务提交,所以Insert update log在事务提交的时候就会被删除
Insert undo log并不会形成版本链,因为插入嘛,肯定是新数据,所以新生成的行数据会表上插入的事务id,不影响前面提到的版本链匹配。