MVCC
MVC概念
MVCC MVCC,全称 Multi-Version Concurrency Control ,即多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。
MVCC在mysql InnoDB引擎中实现了了提高数据库并发性能,能够更加高效的处理读写冲突,做到即使有读写冲突时,也不会加锁,非阻塞并发读。
当前读与快照读
在InnoDB引擎中,存在当前读与快照读
当前读
当前读读取的是数据库当前最新的数据,并且读取时还必须保证其他并发事务不能修改当前记录,所以会对读取的事务进行加锁,是悲观锁的具体实现。
快照读
快照读就是不加锁的查询(select),他不是非阻塞读操作,但前提是事务的隔离级别必须非串行化级别(Serlization),在串行化隔离级别下,快照读会退化为当前读。快照读的实现是基于多版本并发控制(MVCC),可以认为MVCC是行锁的一种变种,但是在很多场景下,MVCC避免了加锁操作,提高了并发性能。但是由于是基于多版本的数据,所以快照读读取的数据可能并不是当前最新的数据,也有可能是历史版本。
MVCC就是维持一个数据的多个版本,使得读写操作没有冲突,只是一个概念,并没有具体的实现。而MySQL中的快照读就是MVCC模型的其中一个非阻塞读功能。
MVCC的应用场景
数据库并发场景:
-
读-读
:不存在任何问题,也不需要并发控制 -
读-写
:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 -
写-写
:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
MVCC能够实现读写冲突的无锁并发控制,为每个事务分配自增的时间戳,为每一次修改的数据都保留一个相应版本,读操作读取该事务开始前的数据的快照。所以MVCC可以为数据库解决一下问题:
在并发读写数据库是,可以做到在读操作时不需要阻塞写操作,写操作也不需要阻塞读操作,提高了数据库的读写并发性能。
还解决了脏读、幻读、不可重复读等事务隔离问题,但是不能解决更新丢失的问题。
MVCC实现原理
MVCC的实现主要依赖于记录中的三个隐式字段,undo日志,Read View来实现。
隐式字段
每行记录除了我们自己的字段外,还有数据隐式定义的字段:DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID。
DB_TRX_ID
6 byte,最近修改(修改/插入)事务 ID:记录创建这条记录/最后一次修改该记录的事务 ID
DB_ROLL_PTR
7 byte,回滚指针,指向这条记录的上一个版本(存储于 rollback segment 里)
DB_ROW_ID
6 byte,隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以DB_ROW_ID产生一个聚簇索引
实际还有一个删除 flag 隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除 flag 变了
如图,DB_ROW_ID是数据库默认为改行生成的唯一隐式主键,DB_TRX_ID是当前操作该记录的事务ID,BDB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个版本(感觉跟git的版本控制的指针很像)。
执行流程
-
比如有一个事务插入Person表一条新纪录,如下图。
-
然后又有一个事务1对该记录的name进行了修改,改为Tom。
-
在事务 1修改该行(记录)数据时,数据库会先对该行加排他锁
-
然后把该行数据拷贝到 undo log 中,作为旧记录,既在 undo log 中有当前行的拷贝副本
-
拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务 ID 为当前事务 1的 ID, 我们默认从 1 开始,之后递增,回滚指针指向拷贝到 undo log 的副本记录,既表示我的上一个版本就是它
-
事务提交后,释放锁
-
-
又来一个事务2修改该记录,将age修改为30。
-
在事务2修改该行数据时,数据库也先为该行加锁
-
然后把该行数据拷贝到 undo log 中,作为旧记录,发现该行记录已经有 undo log 了,那么最新的旧数据作为链表的表头,插在该行记录的 undo log 最前面
-
修改该行 age 为 30 岁,并且修改隐藏字段的事务 ID 为当前事务 2的 ID, 那就是 2 ,回滚指针指向刚刚拷贝到 undo log 的副本记录
-
事务提交,释放锁
从上面的操作我们可以看出,不同事务或者相同事务对同一记录的修改,会导致该记录的undo日志成为一条版本链表,链首为最旧的记录,链尾为最新的记录。
-
ReadView
ReadView概念
ReadVIew就是事务进行快照读槽中的时候产生的读试图(ReadView),在该事务执行快照读的那一刻,会生成数据库当前的一个快照,记录并维护当前系统活跃事务的ID(DB_TRX_ID)。
所以ReadView是用来做可见性判断的,即当我们某个事务执行快照读的时候,对该记录创建一个 Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
ReadView遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(当前事务ID)取出来,与其他当前活跃事务的ID作比较(由ReadView维护,这里假设为trx_list),如果DB_TRX_ID跟ReadView的属性做了比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo log中的DB_TRX_ID做比较,即遍历链表的DB_TRX_ID(链尾到链首),直到找到满足特定条件的DB_TRX_ID,那么这个DB_TRX_ID所在的旧纪录就是当前事务能够看见的最新老版本(对于当前事务可见的最新版本
)。
具体可见性算法如下(不完全)
在解析之前,我们可以把 Read View 简单的理解成有三个全局属性
trx_list(假设)
一个数值列表
用于维护 Read View 生成时刻系统正活跃的事务 ID 列表
up_limit_id
是 trx_list 列表中事务 ID 最小的 ID
low_limit_id
ReadView 生成时刻系统尚未分配的下一个事务 ID ,也就是 目前已出现过的事务 ID 的最大值 + 1
为什么是 low_limit ? 因为它也是系统此刻可分配的事务 ID 的最小值
RC , RR 级别下的 InnoDB 快照读有什么不同?
正是 Read View 生成时机的不同,从而造成 RC , RR 级别下快照读的结果的不同
在 RR 级别下的某个事务的对某条记录的第一次快照读会创建一个快照及 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个 Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个 Read View,所以对之后的修改不可见;
即 RR 级别下,快照读生成 Read View 时,Read View 会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
而在 RC 级别下的,事务中,每次快照读都会新生成一个快照和 Read View , 这就是我们在 RC 级别下的事务中可以看到别的事务提交的更新的原因
总之在 RC 隔离级别下,是每个快照读都会生成并获取最新的 Read View;而在 RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View, 之后的快照读获取的都是同一个 Read View。
比较规则
- 首先比较DB_TRX_ID < up_limit_id,如果小于,则当前事务能够看到的DB_TRX_ID所在的记录,如果大于则进入下一个判断条件
根据DB_TRX_ID(undo log链尾最新版本ID)与ReadView所维护的up_limit_id作比较,如果小于了,说明了当前事务对该记录的修改已经提交,因为当前事务id比活跃的最小事务id还小,不在活跃的事务之中,也就意味着该事务已经提交或回滚,这时因为已经成功修改,那么应该就是提交成功了。也就是在生成ReadView之前,事务已经提交了。
- 接下来判断 DB_TRX_ID >= low_limit_id , 如果大于等于则代表
DB_TRX_ID
所在的记录在Read View
生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断DB_TRX_ID(undo log链尾最新版本ID)大于了Read View里系统待分配的下一个事务id,说明修改该行的事务是生成该Read View之后出现的事务,因为Read View系统待分配的下一个事务id被用了,才会出现比该事务id大的事务。这时,也应该是不可见的,一个事务怎么可以看到后面新来事务做的修改了。
- 判断 DB_TRX_ID 是否在活跃事务之中,trx_list.contains (DB_TRX_ID),如果在,则代表我 Read View 生成时刻,你这个事务还在活跃,还没有 Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在 Read View 生成之前就已经 Commit 了,你修改的结果,我当前事务是能看见的。
如果DB_TRX_ID(undo log链尾最新版本ID)在up_limit_id和low_limit_id之间,那么判断DB_TRX_ID是否在活跃事务列表中,如果没有,则说当前事务已经提交,是能可见。反之,说明当前事务还没有提交,是不可见的。
针对上面三种情况,下面举例说明:
初始化数据100
提问:三次select amount分别读到的数据是多少?
先把undo表里存的版本链已经当前记录表示出来
第一次快照读
此时版本链应该是 事务1 → 事务3 → 初始化
事务3先修改 并且提交
事务1再修改版本但是没有提交,此时的最新记录就是事务1的版本
然后进行快照读 取得最新事务ID(DB_TRX_ID)为1
生成ReadView(up_limit_id(1),low_limit_id(4),取得trx_list(1))
此时DB_TRX_ID 为1,等于up_limit_id(1),小于low_limit_id(4),包含在trx_list中,不可见,回滚。
回滚到事务3,此时的DB_TRX_ID为3,大于up_limit_id(1),小于low_limit_id(4),并且不包含在trx_list中,所以当前记录是可以被看到的,故取得记录为200。
第二次快照读
此时版本链应该是 事务1 → 事务3 → 初始化,虽说事务4开启,但是目前并没有对记录进行修改
此时再第一次快照读的基础上,事务1提交了。不过undo表与当前行数据无变化,对事务1的Read View的数据也不会变化,因为RR模式下,Read View 只会在第一次快照读时生成,后面几次快照读不会生成新的 Read View,也不会改动之前Read View的值。
当前行数据与
Read View
都无变化,那么可见性判断也同①一致,读取到的金额为200。
第三次快照读
此时版本链应该是 事务4 → 事务1 → 事务3 → 初始化
此时再第一次快照读的基础上,事务4修改了数据并提交了
进行快照读 取得最新事务ID(DB_TRX_ID)为4
延用第一次生成的ReadView(up_limit_id(1),low_limit_id(4),取得trx_list(1))
用DB_TRX_ID作比较,大于up_limit_id, 小于low_limit_id(1),不包含在trx_list中,所以该记录不可见,回滚至上一版本
回滚到事务1版本,DB_TRX_ID为(1),等于up_limit_id(1),小于low_limit_id(4),包含在trx_list中,不可见,继续回滚。
回滚到事务3版本,DB_TRX_ID为(3),大于up-limit_id(1),小于low_limit_id(4),不包含在trx_list中,可见,所以得到记录为事务3版本,200。
个人总结
-
Mysql InnoDB通过快照读实现了MVCC模型的非阻塞读功能,实现了读写不加锁,非阻塞并发读,提高了MySQL并发性能。
-
ReadView可见性的三个判断约束了一件事,只有在本事务生成 Read View 之前就已经提交的事务的修改才可以被看见,其他的无论是正在进行的事务的修改还是之后再提交的事务的修改都不可见。
-
在RC和RR不同隔离级别下,相差是很大,关系到是否生成多个ReadView,也就是是否会产生不可重复读的关键,但是在RR级别下,幻读依旧geren
感谢观看,若有错误,敬请指正!
本文借鉴至