MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。

基础概述

数据库并发场景大致分为三种:

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

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

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

既然MVCC可以解决数据库的并发的相关问题,那对于其原理的理解就很重要。不过在学习MVCC多版本并发控制之前,我们必须先了解一下,什么是MySQL InnoDB下的当前读和快照读。

  • 当前读

    • 像select lock in share mode(共享锁), select for update, update, insert ,delete(排他锁)这些操作都是一种当前读,
    • 它读取的是记录的最新版本,所以叫当前读。读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
  • 快照读

    • 像不加锁的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的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。

隐式字段

每行记录其实除了我们在数据库中定义的列之外,每一行中还包含了几个数据库隐藏列,分别是DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID。假设有一张person表,里面包含name和age两个字段,插入一条记录如下图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID是当前操作该记录的事务ID,而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本,这三个字段在实际数据库中是看不到的。

  • DB_TRX_ID

    • 6byte,一个事务对某个表执行了增、删、改操作,分配这条记录的事务ID(最近修改(修改/插入)事务ID);
    • 对于只读事务来说,只有在它第一次对某个用户创建的「临时表执行增、删、改操作」时才会为这个事务分配一个事务id,否则的话是不分配事务id的;
    • 对于读写事务来说,只有在它「第一次对某个表(包括用户创建的临时表)执行增、删、改操作」时才会为这个事务分配一个事务id,否则的话也是不分配事务id的;
    • 有的时候虽然我们开启了一个读写事务,但是在这个事务中全是查询语句,并没有执行增、删、改的语句,那也就意味着这个事务并不会被分配一个事务id。
  • DB_ROLL_PTR

    • 7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里),即指向该记录对应的undo log。
  • DB_ROW_ID

    • DB_ROW_ID是6byte,行记录的唯一标志,这一列不是必须的;
    • MySQL会优先使用用户自定义主键作为主键,如果用户没有定义主键,则选取一个Unique键作为主键,如果表中连Unique键都没有定义的话,则InnoDB会为表默认添加一个名为DB_ROW_ID的隐藏列作为主键;
    • 只有在表中既没有定义主键,也没有申明唯一索引的情况MySQL才会添加这个隐藏列。
  • 实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了

undo日志

对于undo日志的具体介绍之前写过文章MYSQL专题-MySQL三大日志binlog、redo log和undo log,大家想要更好的了解可以去看看,这里再做一下简单介绍。undo log主要分为两种,insert undo log和update undo log。

  • insert undo log

    • 事务在insert新记录时产生的undo log;
    • 只在事务回滚时需要,并且在事务提交后可以被立即丢弃。
  • update undo log

    • 事务在进行update或delete时产生的undo log;
    • 不仅在事务回滚时需要,在快照读时也需要,不能随便删除。只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除。

前面提到,还有一个删除flag隐藏字段。为了实现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可见,那么这条记录一定是可以被安全清除的。

我们以实际例子来看一下它的执行流程。比如有个事务往person表插入一条新记录,记录如下,name为Jack, age为25岁,隐式主键是1,我们假设事务ID为0,和回滚指针为NULL:

现在又来了一个事务对该记录的name做出了修改,改为Jim,则它过程大致如下:

  1. 事务修改该行(记录)数据时,数据库会先对该行加排他锁;
  2. 把该行数据拷贝到undo log中,作为旧记录,既在undo log中有当前行的拷贝副本;
  3. 拷贝完毕后,修改该行name为Jim,并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,既表示我的上一个版本就是它;
  4. 事务提交后,释放锁。

则此时的对应关系如下图所示:

又来了个事务修改person表的同一个记录,将age修改为30岁,执行过程类似上一步:

  1. 事务修改该行数据时,数据库也先为该行加锁;
  2. 然后把该行数据拷贝到undo log中,作为旧记录,发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录的undo log最前面;
  3. 修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务的ID, 那就是2,回滚指针指向刚刚拷贝到undo log的副本记录;
  4. 事务提交,释放锁。

则此时的对应关系如下图所示:

我们可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录。

Read View(读视图)

  • 对于使用READ UNCOMMITTED隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了;
  • 对于使用SERIALIZABLE隔离级别的事务来说,MySQL规定使用加锁的方式来访问记录;
  • 对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的。

为了解决哪个版本是当前事务可见的,MySQL提出了一个ReadView(快照)的概念,在Select操作前会为当前事务生成一个快照,然后根据快照中记录的信息来判断当前记录是否对事务是可见的,如果不可见那么沿着版本链继续往上找,直至找到一个可见的记录。

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

m_ids:生成ReadView时当前系统中活跃的读写事务的事务id列表
min_trx_id:生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
max_trx_id:生成ReadView时系统中应该分配给下一个事务的id值
creator_trx_id:生成该ReadView的事务的事务id

注意max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4,creator_trx_id就是3。我们前边说过,只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0,即creator_trx_id为0。

根据当前数据库中运行中的读写事务id,会去生成一个ReadView。然后根据要读取的数据记录中的事务id(方便区别,记为r_trx_id)跟ReadView中保存的几个属性做如下判断:

  1. 如果被访问版本的r_trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问;
  2. 如果被访问版本的r_trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问;
  3. 如果被访问版本的r_trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问;
  4. 如果被访问版本的r_trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下r_trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

整体过程

介绍完隐式字段,undo log, 以及Read View的概念之后,我们来模拟一下整体的流程。假设现在又四个事务,其对应的状态如下表所示:

事务1 事务2 事务3 事务4
事务开始 事务开始 事务开始 事务开始
修改且已提交
进行中 快照读 进行中

根据之前的描述,当事务2对某行数据执行了快照读,数据库为该行数据生成一个Read View读视图,假设当前事务ID为2,此时还有事务1和事务3在活跃中,事务4在事务2快照读前一刻提交更新了,所以Read View记录了系统当前活跃事务1,3的ID,维护列表上m_ids,当前系统中活跃的读写事务中最小的事务id即min_trx_id为1,系统中应该分配给下一个事务的id即max_trx_id为5,该ReadView的事务的事务id即creator_trx_id为2。

因为只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务,所以当前该行当前数据的undo log如下图所示:

快照读的过程是这样的:

  1. 先拿该记录DB_TRX_ID字段记录的事务ID 4去跟Read View的creator_trx_id(2)比较,看是否相等,显示不相等;
  2. 继续跟min_trx_id比较,看4是否小于min_trx_id(1),所以不符合条件;
  3. 继续判断 4 是否大于等于 max_trx_id(5),也不符合条件;
  4. 最后判断4是否处于m_ids(1,3)中的活跃事务, 最后发现事务ID为4的事务不在当前活跃事务列表中, 符合可见性条件。

所以事务4修改后提交的最新结果对事务2快照读时是可见的,所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本。正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同:

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

猜你感兴趣
MYSQL专题-绝对实用的MYSQL优化总结
MYSQL专题-MySQL事务实现原理
MYSQL专题-使用Binlog日志恢复MySQL数据
MYSQL专题-MySQL三大日志binlog、redo log和undo log

更多文章请点击:更多…

MYSQL专题-MVCC多版本并发控制相关推荐

  1. MySQL 高级 —— MVCC 多版本并发控制

    引言 MySQL的大多数事务型存储引擎实现的都不是简单的行级锁.基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制--MVCC.包括其他数据库如Oracle等,由于MVCC并没有一个统一的实现 ...

  2. 【MySQL】MVCC多版本并发控制(重点:MVCC实现原理之ReadView)

    [大家好,我是爱干饭的猿,本文重点介绍MySQL的MVCC概念.快照读与当前读.MVCC实现原理之ReadView.隐藏字段.Undo Log版本链. 后续会继续分享MySQL和其他重要知识点总结,如 ...

  3. MySQL数据库MVCC多版本并发控制简介

          MVCC (Multiversion Concurrency Control),即多版本并发控制技术,它使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是 ...

  4. 【SQL】MVCC 多版本并发控制

    MVCC多版本并发控制 快照读与当前读 隔离级别 隐藏字段,undo log 版本链 隐藏字段trx_id 版本链 read view 举例说明 read committed(读已提交)隔离级别下 r ...

  5. 【MVCC多版本并发控制】MVCC 机制的原理及实现,什么是MVCC,多版本并发控制

    什么是 MVCC MVCC (Multiversion Concurrency Control) 中文全程叫多版本并发控制,是现代数据库(包括 MySQL.Oracle.PostgreSQL 等)引擎 ...

  6. MySQL MVCC多版本并发控制(脏读和不可重复读解决原理)

    文章目录 一.MVCC概念 二.MVCC应用于已提交读隔离级别 1. 解决脏读 2. 无法解决不可重复读 3. 无法解决幻读 三.MVCC应用于可重复读隔离级别 1. 解决脏读 2. 解决不可重复读 ...

  7. MySQL第一讲 一遍让你彻底掌握MVCC多版本并发控制机制原理

    Mysql在可重复读隔离级别下,同样的sql查询语句在一个事务里多次执行查询结果相同,就算其它事务对数据有修改也不会影响当前事务sql语句的查询结果.这个隔离性就是靠MVCC(Multi-Versio ...

  8. 并发控制:(三)MVCC 多版本并发控制

    1.概述: 定义:Multiversion concurrency control, is a concurrency control method commonly used by database ...

  9. MYSQL专题-由简到繁理解索引结构

    大家可能都听过数据库索引,当然作为开发者来说其实大部分时间也用过索引.但是可能有的人知道索引是干什么的,但是对于索引的结构却不是很了解.所以这篇博客我会谈谈对索引结构的一些知识以及分享如何从零开始一层 ...

最新文章

  1. TF-IDF与余弦相似性的应用
  2. Java设计模式(代理模式-模板方法模式-命令模式)
  3. “内卷化”的快手与抖音——2020年短视频的“无聊经济”往何处去
  4. 【算法漫画】什么是红黑树?(下篇)
  5. .NET HttpGet 获取服务器文件下的图片信息 同步和异步方式处理
  6. 边工作边刷题:70天一遍leetcode: day 7
  7. ajax 入参为list_ajax向后台传递list参数
  8. IOS中的数据存储 简单总结
  9. Coolite Toolkit学习笔记四:容器控件之FiledSet、Panel和Window
  10. Cache总容量计算与写回法联合使用
  11. java-Apache Commons IO
  12. mysql导入数据库没创建表_mysqldump不会创建表或导入任何数据
  13. 《Hadoop权威指南》知识点整理3
  14. Drug X跨越鸿沟:一个生物科学家的新药研发跋涉记
  15. 05因果图法和决策表法
  16. python用pil图像放大缩小_Python 使用PIL中的resize进行缩放的实例讲解
  17. cpp简单实现一下RNN神经网络
  18. 御坂坂的C++学习之路(1)
  19. C# FileInfo
  20. 算法中的一些数学问题分享,ICG游戏

热门文章

  1. 如何在C语言中巧妙地避免使用if语句?
  2. 线性筛素数(欧拉筛)
  3. MoeCTF 2021Re部分------Algorithm_revenge
  4. angr学习笔记(9)(添加约束)
  5. PE学习(九)第九章:TLS 动态TLS与静态TLS
  6. 7、调用存储过程和函数
  7. 1.14 实例:完善除法运算的错误信息
  8. 限制用户不允许输入中文字符
  9. C语言希尔排序(解析)
  10. Linux之文件压缩与打包