MVCC又叫多版本并发控制,用来解决脏读,不可重复读,以及配合锁解决幻读问题,在保证隔离性基础上,提升了读取效率和并发性。
(一)版本链
MVCC是基于一个undolog版本链和readview来实现的,我们知道undolog是可以保证我们的原子性的,作为一个回滚日志,不仅记录了我们dml操作之前的数据,同样记录了我们操作的一个反向操作,此外每条undolog还记录一个叫做roll_pointer这个引用信息,通过这个引用信息,我们可以简单把他理解为链表中的next(指向下一个节点),我们就可以把某一条数据对应的undolog日志组织成一个undo链,这样这个undo链就记录这这个数据的变化
如下所示
我们看到每个数据行存储了对应的主键id,以及是那个事务修改的对应值的事务id,一个roll_pointer指向下一个数据行,之后就全是数据了
每当有一条数据被修改,都会有一个版本链,体现了这条记录的所有变更,当事务对这个条记录进行了修改,我们就会把修改后的数据链,连接到版本链的头部
注:这里我们能看到是存储了事务id的,那么也就是说,版本链是在所有事务中共享,也就是说所有事务访问的都是同一条版本链
(二)ReadView
每条数据的版本链都构造好之后,我们查询时要访问版本链的哪一行数据,我们就需要使用ReadView结构来实现,其实ReadView就是一个内存结构,本质是一个视图,在事务使用select查询时,我们就会自动生成一个ReadView,我们具体来看一下都有什么
我们根据上面的各种id就能判断,哪些数据值是我们当前这个事务可以获取的,哪些数据值是对我们当前事务不可见的,就比如说我们如果undo链中的一个事务id<活跃集合的最小id,那我们就是可以获取到
我们用一个具体例子来说,就比如我们当前事务id为201,执行select会构造一个readview
此时假设我们活跃事务除了我们还有三个事务(90,100,200)
那我们上面四个参数存的值分别为
此时我们要从版本链中获取数据
我们不可以获取活跃事务id的数据,所以我们目前只能获取比最小活跃id小的(已提交事务)的一个数据
我们获取的规则总结如下:
从undo链头来遍历所有版本
第一步:判断该版本是否为当前事务创建(当前事务创建的数据是可以获取到的,是为了保证我们事务内数据的一致性),也就是m_creator_trx_id等于该版本事务id,意味着读自己修改数据,可以直接访问,如果不是则看第二步
第二步:若该版本事务id<m_up_limit_id(最小事务id),意味着在该版本在readView生成之前已经提交,可以直接访问,如果不是则看第三步
第三步:或该版本事务id>=m_low_limit_id(最大事务id),意味着该版本在readview之后才创建,那么我们是读不到的,直接遍历下一个版本,无需其他判断
第四步:如果该版本的事务id在m_up_limit_id和m_low_limit_id之间,同时不在活跃事务列表中,那么就说明我们创建readview时,这个事务就已经提交了,可以访问,无需其他遍历
这样我们从版本链头遍历一直到链尾,找到每一个符合要求的版本就可以,所以查找到的事务都是已经提交的事务,这样就避免了 “ 脏读 ”
MVCC解决问题
脏读:我们mvcc通过各个id来保障我们读到的undo链中的数据行都是已经提交的事务,所以不会读到未提交的事务,来确保我们不出现脏读问题
不可重复读:我们刚刚说readview在可重复读下,只会在第一次select时创建,以后我们每一次select都会读取这一个readview中的数据,这样就不存在我们在同一个事务中两次读取同一条数据会出现数据不一样的问题了
注:我们说只有在可重复读以及以上是只创建一个readView的,如果是读已提交,那么每一次select都会创建一个readView,此时会出现不可重复读的问题
幻读:我们幻读是无法单独通过mvcc来解决的,我们需要通过锁配合mvcc来解决,mvcc来确保稳定的数据版本,而锁(next-key)来保证我们的我们的间隙不被插入,防止我们多次查询出的数据集不同
注:我们说可重复读下,我们只创建一个readView,之后每一次查询都会使用我们第一次创建的readview,那么我们如果当前事务进行了修改,我们能够正确的读到信息吗?
答案是可以的,这是因为我们要确保事务内数据的一致性