MVCC多版本并发控制
!首先声明,MySQL的测试环境是5.7算法
前提概要
什么是MVCC
什么是当前读和快照读?
当前读,快照读和MVCC的关系
MVCC实现原理
隐式字段
undo日志
Read View(读视图)
总体流程
MVCC相关问题
RR是如何在RC级的基础上解决不可重复读的?
RC,RR级别下的InnoDB快照读有什么不一样?数据库
前提概要
什么是MVCC?
MVCC
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,通常在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
mvcc - @百度百科编程
MVCC在MySQL InnoDB中的实现主要是为了提升数据库并发性能,用更好的方式去处理读-写冲突,作到即便有读写冲突时,也能作到不加锁,非阻塞并发读安全
什么是当前读和快照读?
在学习MVCC多版本并发控制以前,咱们必须先了解一下,什么是MySQL InnoDB下的当前读和快照读?并发
当前读
像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操做都是一种当前读,为何叫当前读?就是它读取的是记录的最新版本,读取时还要保证其余并发事务不能修改当前记录,会对读取的记录进行加锁mvc
快照读
像不加锁的select操做就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之因此出现快照读的状况,是基于提升并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,能够认为MVCC是行锁的一个变种,但它在不少状况下,避免了加锁操做,下降了开销;既然是基于多版本,即快照读可能读到的并不必定是数据的最新版本,而有多是以前的历史版本编程语言
说白了MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读其实是一种加锁的操做,是悲观锁的实现高并发
当前读,快照读和MVCC的关系
准确的说,MVCC多版本并发控制指的是 “维持一个数据的多个版本,使得读写操做没有冲突” 这么一个概念。仅仅是一个理想概念
而在MySQL中,实现这么一个MVCC理想概念,咱们就须要MySQL提供具体的功能去实现它,而快照读就是MySQL为咱们实现MVCC理想模型的其中一个具体非阻塞读功能。而相对而言,当前读就是悲观锁的具体功能实现
要说的再细致一些,快照读自己也是一个抽象概念,再深刻研究。MVCC模型在MySQL中的具体实现则是由 3个隐式字段,undo日志 ,Read View 等去完成的,具体能够看下面的MVCC实现原理
MVCC能解决什么问题,好处是?
数据库并发场景有三种,分别为:性能
读-读:不存在任何问题,也不须要并发控制
读-写:有线程安全问题,可能会形成事务隔离性问题,可能遇到脏读,幻读,不可重复读
写-写:有线程安全问题,可能会存在更新丢失问题,好比第一类更新丢失,第二类更新丢失
MVCC带来的好处是?
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增加的时间戳,为每一个修改保存一个版本,版本与事务时间戳关联,读操做只读该事务开始前的数据库的快照。 因此MVCC能够为数据库解决如下问题学习
在并发读写数据库时,能够作到在读操做时不用阻塞写操做,写操做也不用阻塞读操做,提升了数据库并发读写的性能
同时还能够解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
小结一下咯
总之,MVCC就是由于大牛们,不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出的解决方案,因此在数据库中,由于有了MVCC,因此咱们能够造成两个组合:
MVCC + 悲观锁
MVCC解决读写冲突,悲观锁解决写写冲突
MVCC + 乐观锁
MVCC解决读写冲突,乐观锁解决写写冲突
这种组合的方式就能够最大程度的提升数据库并发性能,并解决读写冲突,和写写冲突致使的问题
MVCC的实现原理
MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。因此咱们先来看看这个三个point的概念
隐式字段
每行记录除了咱们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段
DB_TRX_ID
6byte,最近修改(修改/插入)事务ID:记录建立这条记录/最后一次修改该记录的事务ID
DB_ROLL_PTR
7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
DB_ROW_ID
6byte,隐含的自增ID(隐藏主键),若是数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
实际还有一个删除flag隐藏字段, 既记录被更新或删除并不表明真的删除,而是删除flag变了
如上图,DB_ROW_ID是数据库默认为该行记录生成的惟一隐式主键,DB_TRX_ID是当前操做该记录的事务ID,而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本
undo日志
undo log主要分为两种:
insert undo log
表明事务在insert新记录时产生的undo log, 只在事务回滚时须要,而且在事务提交后能够被当即丢弃
update undo log
事务在进行update或delete时产生的undo log; 不只在事务回滚时须要,在快照读时也须要;因此不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
purge
从前面的分析能够看出,为了实现InnoDB的MVCC机制,更新或者删除操做都只是设置一下老记录的deleted_bit,并不真正将过期的记录删除。
为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了避免影响MVCC的正常工做,purge线程本身也维护了一个read view(这个read view至关于系统中最老活跃事务的read view);若是某个记录的deleted_bit为true,而且DB_TRX_ID相对于purge线程的read view可见,那么这条记录必定是能够被安全清除的。
对MVCC有帮助的实质是update undo log ,undo log实际上就是存在rollback segment中旧记录链,它的执行流程以下:
1、 好比一个有个事务插入persion表插入了一条新记录,记录以下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,咱们假设为NULL
2、 如今来了一个事务1对该记录的name作出了修改,改成Tom
在事务1修改该行(记录)数据时,数据库会先对该行加排他锁
而后把该行数据拷贝到undo log中,做为旧记录,既在undo log中有当前行的拷贝副本
拷贝完毕后,修改该行name为Tom,而且修改隐藏字段的事务ID为当前事务1的ID, 咱们默认从1开始,以后递增,回滚指针指向拷贝到undo log的副本记录,既表示个人上一个版本就是它
事务提交后,释放锁
3、 又来了个事务2修改person表的同一个记录,将age修改成30岁
在事务2修改该行数据时,数据库也先为该行加锁
而后把该行数据拷贝到undo log中,做为旧记录,发现该行记录已经有undo log了,那么最新的旧数据做为链表的表头,插在该行记录的undo log最前面
修改该行age为30岁,而且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,回滚指针指向刚刚拷贝到undo log的副本记录
事务提交,释放锁
从上面,咱们就能够看出,不一样事务或者相同事务的对同一记录的修改,会致使该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最先的旧记录(固然就像以前说的该undo log的节点多是会purge线程清除掉,向图中的第一条insert undo log,其实在事务提交以后可能就被删除丢失了,不过这里为了演示,因此还放在这里)
Read View(读视图)
什么是Read View?
什么是Read View,说白了Read View就是事务进行快照读操做的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每一个事务开启时,都会被分配一个ID, 这个ID是递增的,因此最新的事务,ID值越大)
因此咱们知道 Read View主要是用来作可见性判断的, 即当咱们某个事务执行快照读的时候,对该记录建立一个Read View读视图,把它比做条件用来判断当前事务可以看到哪一个版本的数据,既多是当前最新的数据,也有多是该行记录的undo log里面的某个版本的数据。
Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其余活跃事务的ID去对比(由Read View维护),若是DB_TRX_ID跟Read View的属性作了某些比较,不符合可见性,那就经过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到知足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本
那么这个判断条件是什么呢?
咱们这里盗窃@呵呵一笑百媚生一张源码图,如上,它是一段MySQL判断可见性的一段源码,即changes_visible方法(不彻底哈,但能看出大体逻辑),该方法展现了咱们拿DB_TRX_ID去跟Read View某些属性进行怎么样的比较
在展现以前,我先简化一下Read View,咱们能够把Read View简单的理解成有三个全局属性
trx_list(名字我随便取的)
一个数值列表,用来维护Read View生成时刻系统正活跃的事务ID
up_limit_id
记录trx_list列表中事务ID最小的ID
low_limit_id
ReadView生成时刻系统还没有分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1
首先比较DB_TRX_ID < up_limit_id, 若是小于,则当前事务能看到DB_TRX_ID 所在的记录,若是大于等于进入下一个判断
接下来判断 DB_TRX_ID 大于等于 low_limit_id , 若是大于等于则表明DB_TRX_ID 所在的记录在Read View生成后才出现的,那对当前事务确定不可见,若是小于则进入下一个判断
判断DB_TRX_ID 是否在活跃事务之中,trx_list.contains(DB_TRX_ID),若是在,则表明我Read View生成时刻,你这个事务还在活跃,尚未Commit,你修改的数据,我当前事务也是看不见的;若是不在,则说明,你这个事务在Read View生成以前就已经Commit了,你修改的结果,我当前事务是能看见的
总体流程
咱们在了解了隐式字段,undo log, 以及Read View的概念以后,就能够来看看MVCC实现的总体流程是怎么样了
总体的流程是怎么样的呢?咱们能够模拟一下
当事务2对某行数据执行了快照读,数据库为该行数据生成一个Read View读视图,假设当前事务ID为2,此时还有事务1和事务3在活跃中,事务4在事务2快照读前一刻提交更新了,因此Read View记录了系统当前活跃事务1,3的ID,维护在一个列表上,假设咱们称为trx_list
事务1 事务2 事务3 事务4
事务开始 事务开始 事务开始 事务开始
… … … 修改且已提交
进行中 快照读 进行中
… … …
Read View不只仅会经过一个列表trx_list来维护事务2执行快照读那刻系统正活跃的事务ID,还会有两个属性up_limit_id(记录trx_list列表中事务ID最小的ID),low_limit_id(记录trx_list列表中事务ID最大的ID,也有人说快照读那刻系统还没有分配的下一个事务ID也就是目前已出现过的事务ID的最大值+1,我更倾向于后者 >>>资料传送门 | 呵呵一笑百媚生的回答) ;因此在这里例子中up_limit_id就是1,low_limit_id就是4 + 1 = 5,trx_list集合的值是1,3,Read View以下图
咱们的例子中,只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务,因此当前该行当前数据的undo log以下图所示;咱们的事务2在快照读该行记录的时候,就会拿该行记录的DB_TRX_ID去跟up_limit_id,low_limit_id和活跃事务ID列表(trx_list)进行比较,判断当前事务2能看到该记录的版本是哪一个。
因此先拿该记录DB_TRX_ID字段记录的事务ID 4去跟Read View的的up_limit_id比较,看4是否小于up_limit_id(1),因此不符合条件,继续判断 4 是否大于等于 low_limit_id(5),也不符合条件,最后判断4是否处于trx_list中的活跃事务, 最后发现事务ID为4的事务不在当前活跃事务列表中, 符合可见性条件,因此事务4修改后提交的最新结果对事务2快照读时是可见的,因此事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本
也正是Read View生成时机的不一样,从而形成RC,RR级别下快照读的结果的不一样
MVCC相关问题
RR是如何在RC级的基础上解决不可重复读的?
当前读和快照读在RR级别下的区别:
表1:
事务A 事务B
开启事务 开启事务
快照读(无影响)查询金额为500 快照读查询金额为500
更新金额为400
提交事务
select 快照读金额为500
select lock in share mode当前读金额为400
在上表的顺序下,事务B的在事务A提交修改后的快照读是旧版本数据,而当前读是实时新数据400
表2:
事务A 事务B
开启事务 开启事务
快照读(无影响)查询金额为500
更新金额为400
提交事务
select 快照读金额为400
select lock in share mode当前读金额为400
而在表2这里的顺序中,事务B在事务A提交后的快照读和当前读都是实时的新数据400,这是为何呢?
这里与上表的惟一区别仅仅是表1的事务B在事务A修改金额前快照读过一次金额数据,而表2的事务B在事务A修改金额前没有进行过快照读。
因此咱们知道事务中快照读的结果是很是依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读的地方很是关键,它有决定该事务后续快照读结果的能力
咱们这里测试的是更新,同时删除和更新也是同样的,若是事务B的快照读是在事务A操做以后进行的,事务B的快照读也是能读取到最新的数据的
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。---------------------