MVCC,即多版本并发控制(Multi-Version Concurrency Control)指的是,经过版本链维护一个数据的多个版本,使得读写操做没有冲突,可保证不一样事务读写、写读操做并发执行,提升系统性能。面试
实际上,innodb中“读已提交”和“可重复读”这两种隔离级别的事务在查询数据时访问版本链的过程,是基于这套原理。本文将总结MVCC机制底层原理,并解释它是如何解决“脏读”和“不可重复读”问题的。算法
感受如今每总结一个知识点,老是会引出一堆相关知识,学习真的是永无止境~。首先介绍一下几种并发事务问题,和四种隔离级别,这与后文原理介绍密不可分。并且,毕竟都是面试高频考点,尊重一下。sql
innodb中采用了next-key-lock锁算法避免了幻读,使得“可重复读”级别也达到了“串行化”级别的效果数据库
咱们先设定一个场景:数组
假设数据库表中存在一条记录row_old,这时事务A和事务B同时begin,事务A将该记录修改成了row_new,事务B读取行记录,事务A提交,事务B再次读取这条行记录。并发
本文中将使用该场景来分析“脏读”和“不可重复读”现象。mvc
若事务B在A提交前读到row_new,即出现“脏读”现象;若事务B在A提交后读到row_new,即出现“不可重复读”现象。性能
可是,正常状况是,不管事务A是否提交,事务B读取该条记录,都只能读出row_old。学习
什么方法能够达到这种效果呢?能够很直观地想到,将事务A修改后的版本存起来。那么又有一系列问题,如何存,用什么结构来存?版本链即是为此而引入的。指针
版本链,实际上就是一条存储多个版本行记录的链表。数据库中的每一行数据都对应一个版本链。链表中每个结点表明一个行记录。行记录中有两个重要的隐藏字段:
版本链的最底层即为数据表中最原始的行记录,上层存储各个事务修改后的行记录,逐个用回滚指针相链接。版本链示意图以下所示:
还有一个问题,版本链是存储在哪的?没错,咱们熟悉的undo log回滚日志就是用来存储版本链的 。
若是当前事务修改一条记录,这条更新过的记录被记录到版本链中,对于当前事务而言,因为自身事务id和版本链中最新一条行记录的trx_id相匹配,因此能够将其读取出来。可是对于其它事务而言,是不但愿能读出这条记录的,而是但愿它能顺着版本链,找出本身须要的版本的行记录。
那么如何找到正确的版本?这里涉及到一个快照机制。事务在执行select语句时,会生成一个一致性视图:read-view,至关于一个快照,记录正在活跃的事务的编号。
read-view里面包含一个数组,m_ids,该数组记录(产生快照的这一时刻)版本链中未提交的每一个版本的trx_id组成的序列。同时,read-view还会记录一个最大已建立事务id,即 max_id,以及数组中最小id即 min_id。查询版本链时,会将行记录中的trx_id与read-view中的max_id、min_id、m_ids[]等进行比对。依据以下版本比对规则来进行比对。
补充:删除的原理:
删除能够认为是update的特殊状况。假如要删除一行记录,会将版本链上最新一条记录复制一份,将行格式头信息中(record header)里面的(deleted flag)标志位置为true,表示当前记录已被删除。若顺着版本链访问到这条记录,(deleted flag)标志位为true,表示记录已删除,不返回数据。
让咱们再回到前文提到的场景:事务A将行记录row_old修改成了row_new,未提交时,row_new行记录已经加入到了版本链,而且记录了事务A的id。此时事务B开始查询,生成快照read-view,其中的m_ids记录了未提交版本的trx_id,包括row_new的id。当查询到row_new时,其trx_id在m_ids数组中,根据版本链比对规则,其对B事务不可见,所以继续向下查找,直到找出row_old。
综上所述,read-view快照机制加上版本链匹配规则,能够杜绝“脏读”现象。
根据上文的分析,咱们对MVCC机制有了一个清晰的了解。在“读已提交”隔离级别就是基于这个原理来解决“脏读”问题的。而“可重复读”隔离级别却与之不尽相同,差异以下:
再次回到上文中提到的情景,假设事务A修改将row_old修改成row_new,未提交时,事务B开始执行select,生成read-view,这时事务A进行提交,而后事务B再次select,这时依然沿用上一次的read-view,row_new的id依然是记录在m_ids数组中的,因此事务B只能读取到row_old,两次读取都只能读出row_old。
这里我但愿再补充一种状况:B事务还没有提交结束时,再开启一个事务C,修改row_new为row_new_c,并提交,这时版本链中新增一个row_new_c结点,记录C的id。事务B再次select,依然只能读取到row_old。由于在版本链中遍历至row_new_c时,会触发“版本对比规则”的第二条,该条记录对事务B不可见,所以继续向下查找直到找出row_old。
因此,综上所述,不管版本链发生何种改变,只要在单次事务中read-view固定不变,读取到的数据必定是维持在同一个版本。在“可重复读”级别中,就是经过沿用第一次read-view快照的方法,解决了“不可重复读”问题。