ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

MVCC原理解析

2021-07-01 20:29:43  阅读:165  来源: 互联网

标签:事务 快照 记录 DB MVCC 原理 解析 ID


一、什么是MVCC?

MVCC
MVCC即多版本并发控制。一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。

说白了MVCC就是为了实现解决读-写冲突问题时不加锁的操作,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现。

二、什么是当前读和快照读?

在学习MVCC多版本并发控制之前,我们必须先了解一下,什么是MySQL InnoDB下的当前读和快照读?

  • 当前读
    像select lock in share mode(共享锁);select for update,update,insert,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
  • 快照读
    像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。

当前读是通过锁,快照读是通过MVCC。

三、MVCC能解决什么问题,好处是?

数据库并发场景有三种,分别为:

  • 读-读:不存在任何问题,也不需要并发控制。
  • 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读。
  • 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失。

MVCC带来的好处是?

多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以MVCC可以为数据库解决以下问题:

  • 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。
  • 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。

说白了,MVCC就是因为大牛们,不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出的解决方案。

四、MVCC的实现原理

MVCC的实现原理主要是依赖记录中的 3个隐式字段undo日志(或者说日志中的版本链)ReadView 来实现的。所以我们先来看看这个三个point的概念。

1.隐式字段

每行记录除了我们自定义的字段外,还有数据库隐式定义的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隐藏字段, 为true时并不代表真的删除,而是删除flag变了(逻辑删除)。

在这里插入图片描述
如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID是当前操作该记录的事务ID,而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本(形成版本链)。

2.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线程自己也维护了一个readview(这个readview相当于系统中最老活跃事务的readview);如果某个记录的deleted_bit为true(条件一),并且DB_TRX_ID相对于purge线程的readview可见(条件二),那么这条记录一定是可以被安全清除的。

也就是说,当当前记录(记录1)执行了更新或者删除操作(变为了记录2),并且记录1(也就是旧数据)的DB_TRX_ID相对于purge线程的readview可见,那记录1就有可能会被从undo日志中清除。

undo log实际上就是存在rollback segment中的旧记录链(undo日志中存的是旧记录链),它的执行流程如下:

一、 比如有个事务1在persion表中插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID为1,假设回滚指针为NULL。
在这里插入图片描述

二、 现在来了一个事务2对该记录的name做出了修改,改为Tom

  • 在事务2修改该行(记录)数据时,数据库会先对该行加排他锁。
  • 然后把该行数据拷贝到undo log中作为旧记录。
  • 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务2的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,表示我的上一个版本就是它。
  • 事务提交后,释放锁。
    在这里插入图片描述
    三、 又来了个事务3修改person表的同一个记录,将age修改为30岁
  • 在事务2修改该行数据时,数据库也先为该行加锁。
  • 然后把该行数据拷贝到undo log中。
  • 修改该行age为30岁,并且修改trx_id(事务ID)为当前事务3的ID, 那就是3,回滚指针指向刚刚拷贝到undo log的副本记录。
  • 事务提交,释放锁。

在这里插入图片描述

从上面我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录(就像之前说的,该undo log的节点可能会被purge线程清除掉,比如向图中的第一条insert undo log,其实在事务提交之后可能就被删除丢失了,不过这里为了演示,所以还放在这里)。

3.ReadView(读视图)

说白了ReadView就是事务进行快照读操作的时候生产的读视图,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。

所以我们知道 ReadView主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个ReadView读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。

Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_IDRead View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本。

从代码层面来说,readview其实就是个对象,其中包含四个属性。
在这里插入图片描述
说明:

  • max trx_id表示ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1
  • 活跃的事务,就是还没有提交的事务。
    在这里插入图片描述

ReadView如何判断版本链中的哪个版本可用?

在这里插入图片描述

  • 首先比较DB_TRX_ID < min trx_id, 如果小于,则当前事务能看到DB_TRX_ID 所在的记录。
  • 接下来判断 DB_TRX_ID > max trx_id , 如果大于等于则代表DB_TRX_ID 所在的记录在Read View生成后才出现的(超出了现在的版本链),那对当前事务肯定不可见
  • 判断min trx_id <= DB_TRX_ID <= max trx_id,这个时候就需要再判断两种情况:(1) 如果这个版本的事务ID在ReadView的未提交事务数组中,表示这个版本是由还未提交的事务生成的,那么就是不可见的;(2)如果这个版本的事务ID不在ReadView的未提交事务数组中,表示这个版本是已经提交了的事务生成的,那么是可见的

数据库有四种常用的隔离级别,读未提交,读已提交,可重复读,串行化读。其中常用的是读已提交和可重复读,而这俩都是基于MVCC实现的。(MVCC其实主要针对的就是RC和RR)

五、MVCC实现的整体流程

例1

首先,假设事务10插入了一条数据。
加粗样式
现在又来了事务20和事务60(事务id其实应该是自增的,这里是随意设的)。
在这里插入图片描述
在经过事务20的两次更新后,版本链如下:
在这里插入图片描述
张三是最开始的一条数据,且它的事务已经提交了。所以按理来说,我们应该是能读出张三这条数据的。下面进行分析。
在这里插入图片描述
ReadView的几个属性值如上图。
在这里插入图片描述
经过大小关系判定,只有10 < 20,满足了条件,说明当前事务能看到张三这条数据,也就是读到了张三这个数据。而并不能读到未提交的那两个事务。

所以通过版本链和ReadView实现了读已提交的过程。

例2

当事务2对某行数据执行了快照读,数据库为该行数据生成一个Read View读视图,假设当前事务ID为2,此时还有事务1和事务3在活跃中,事务4在事务2快照读前一刻提交更新了,所以Read View记录了系统当前活跃事务1,3的ID,维护在一个列表上,我们称为m_ids。
在这里插入图片描述
ReadView的几个属性值如下图。
在这里插入图片描述
只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务,所以当前该行当前数据的undo log如下图所示;我们的事务2在快照读该行记录的时候,就会拿该行记录的DB_TRX_ID去跟up_limit_id,low_limit_id和活跃事务ID列表(m_ids)进行比较,判断当前事务2能看到该记录的版本是哪个。
在这里插入图片描述
所以先拿该记录DB_TRX_ID字段记录的事务ID 4去跟Read Viewmin trx_id比较,看4是否小于min trx_id(1),所以不符合条件,继续判断 4 是否大于 max trx_id(5),也不符合条件,最后判断4是否处于m_ids中的活跃事务, 最后发现事务ID为4的事务不在当前活跃事务列表中, 符合可见性条件,所以事务4修改后提交的最新结果对事务2快照读时是可见的,所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本。

六、MVCC相关问题

1.为什么读已提交没有实现可重复读?

读已提交和可重复度生成ReadView的时机是不同的。读已提交的这个时候,是每次执行select查询的时候,就会生成一个ReadView。比如现在有一个事务,它有两个select,这两个select查询方法会生成两个ReadView。如果在执行第一个select的时候,另一个update事务(增删改都行)还没有提交,所以就读不到最新的(update后的)数据。但是在第二次select的时候,另一个事务已经提交了,这个时候select的语句就会查到另一个事务提交的这个数据,因为它又生成了一个新的ReadView视图。
因为一个事务里的两个select语句查询到了不同的数据,这就违背了可重复读。

2.那RR是如何在RC级的基础上解决不可重复读的?

刚才说了读已提交是以每个select为单位的,每个select都会生成一个ReadView。而可重复读是以一个事务为单位的。比如现在一个事务里有两个select语句,在第一个select的时候生成了一个ReadView,那第二个select语句会使用同一个ReadView,而不会生成新的ReadView。所以再可重复读这个隔离级别下,它的ReadView是事务级的。

总结:RC,RR级别下的InnoDB快照读有什么不同?

正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同。

  • 在RR级别下,某个事务对某条记录的第一次快照读(就是不加锁的select查询语句)会创建一个快照及Read View,,将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见。
  • 而在RC级别下的事务中,每次快照读都会新生成一个快照和Read View(每个select都对应一个ReadView), 这就是我们在RC级别下的事务中可以看到别的事务提交的更新,造成不可重复读的原因。

总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。

3.快照读是如何解决幻读的?

因为开始读的时候,只会生成一份ReadView,不管后面数据怎么增删改,都对ReadView视图没有影响,我读的还是我第一次生成的。

4.当前读如何解决幻读的?

比如在一个事务里面,第一个select语句查询id大于2的数据,查询出来了三条。这个时候有一个写的事务,又插入了一条数据。等读事务中的第二个select语句去查询的时候,查出来id大于2的数据变成了四条,这个时候就发生了幻读。
当前读通过间隙锁的方式解决了幻读。**间隙锁是锁住一段范围。**比如id大于2,那间隙锁会把id大于2的这个范围全部都上锁。这个时候别人就不能往id大于2这个范围里插入数据了。从而解决了幻读。

Mysql的默认隔离级别是可重复读,它默认开启了间隙锁,所以可以解决幻读问题。



参考:

标签:事务,快照,记录,DB,MVCC,原理,解析,ID
来源: https://blog.csdn.net/Shmily_0/article/details/118389631

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有