MySQL / 多版本并发控制
- 前提概要
- 什么是 MVCC
- 什么是当前读和快照读?
- 当前读,快照读和 MVCC 的关系
- MVCC 实现原理
- 隐式字段
- undo 日志
- Read View(读视图)
- 整体流程
- MVCC 相关问题
- RR 是如何在 RC 级的基础上解决不可重复读的?
- RC、RR 级别下的 InnoDB 快照读有什么不同?
一、前提概要
1、什么是 MVCC ?
MVCC,全称 Multi-Version Concurrency Control,即多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
MVCC在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
2、什么是当前读和快照读?
在学习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 实现原理
3、MVCC能解决什么问题,好处是?
数据库并发场景有三种,分别为:
- 读 - 读:不存在任何问题,也不需要并发控制
- 读 - 写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读。
- 写 - 写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失。
4、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 中旧记录链,它的执行流程如下:
一、 比如一个有个事务插入 persion 表插入了一条新记录,记录如下,name为 Jerry,age 为 24 岁,隐式主键是 1,事务 ID 和回滚指针,我们假设为 NULL。
二、 现在来了一个事务 1 对该记录的 name 做出了修改,改为 Tom 。
- 在事务 1 修改该行(记录)数据时,数据库会先对该行加排他锁。
- 然后把该行数据拷贝到 undo log 中,作为旧记录,既在 undo log 中有当前行的拷贝副本。
- 拷贝完毕后,修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID,我们默认从 1 开始,之后递增,回滚指针指向拷贝到 undo log 的副本记录,既表示我的上一个版本就是它
- 事务提交后,释放锁。
三、 又来了个事务 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 。
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:
表2:
而在表 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。
转载于:https://www.jianshu.com/p/8845ddca3b23
(SAW:Game Over!)
MySQL / 多版本并发控制相关推荐
- fetch first mysql_MySQL多版本并发控制机制(MVCC)源码浅析
MySQL多版本并发控制机制(MVCC)-源码浅析 前言 作为一个数据库爱好者,自己动手写过简单的SQL解析器以及存储引擎,但感觉还是不够过瘾.<>诚然讲的非常透彻,但只能提纲挈领,不能让 ...
- mysql事务并发控制_mysql事务和多版本并发控制详解
一.mysql事务 事务就是一组原子性的SQL查询,或者说一个独立的工作单元.如果数据库引擎可以成功执行该组全部语句,那么就执行该组语句.如果其中有任何一条语句不能执行,那么所有的语句都不会执行.也就 ...
- MySQL的多版本并发控制(MVCC)
点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:又一程序员进了ICU:压垮一个家庭,一张结算单就够 个人原创100W+访问量博客:点击前往,查看更多 来源:ht ...
- MYSQL专题-MVCC多版本并发控制
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制.MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内 ...
- Mysql事务隔离级别及MVCC(多版本并发控制)
一.MySQL事务隔离级别 先注明一点:以下讨论都是在多事务并发的情境下讨论的 事务的特性(InnoDB引擎才有事务): ACID 原子性:一个事务不可再分割,要么都执行要么都不执行 一致性:一个事务 ...
- MySQL 高级 —— MVCC 多版本并发控制
引言 MySQL的大多数事务型存储引擎实现的都不是简单的行级锁.基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制--MVCC.包括其他数据库如Oracle等,由于MVCC并没有一个统一的实现 ...
- MySQL MVVC多版本并发控制
一.概述 MVCC(Multiversion Concurrency Control),多版本并发控制.它和undo log中的版本链息息相关,MVVC通过数据行的多个版本来实现数据库的并发控制. 简 ...
- MySQL MVCC多版本并发控制(脏读和不可重复读解决原理)
文章目录 一.MVCC概念 二.MVCC应用于已提交读隔离级别 1. 解决脏读 2. 无法解决不可重复读 3. 无法解决幻读 三.MVCC应用于可重复读隔离级别 1. 解决脏读 2. 解决不可重复读 ...
- MySQL中的MVVC多版本并发控制机制
文章目录 引入 MySQL中MVCC的运用 快照读和当前读 更多文章和干货 引入 MVCC全称是:Multiversion concurrency control,多版本并发控制,提供并发访问数据库时 ...
最新文章
- UE5使用MetaHuman构建超现实的角色
- CCS代码编辑的几个常用设置
- 深度优先算法回溯实例
- CUDA学习(三)之使用GPU进行两个数组相加
- Total = 80是这样计算出来的
- Message Flood
- pandas的自带数据集_pandas.DataFrame.sample随机抽样
- 类、对象以及jvm运行内存解析
- LeetCode 15. 三数之和(3Sum)
- 工程数学(数值分析)第三讲:求解线性代数方程组
- 写了Bug,误执行 rm -fr /*,我删删删删库了,要跑路吗?| 原力计划
- D3.js学习(一)
- mysql创建用并 grant_Mysql创建用户并授权
- CSS中min-height:100%问题
- 离线OCR、文字识别、ios证件扫描、ios系统OCR(ios、android)
- 自建服务器好还是外包,自建与外包对比:如何使你的IT回归内部
- 计算机用户名如何保存,电脑的用户名和密码怎么查看
- Unable to set dev_loss_tmo and fast_io_fail_tmo in multipath.conf
- 幼儿-综合素质【7】
- 2021年中国社会融资规模及融资结构分析:对实体经济发放的人民币贷款占比超60%[图]
热门文章
- 【linux高级程序设计】(第十二章)Linux多线程编程 4
- c++ 6.0 没有找到mspdb60.dll 问题的解决
- linux安装截图讲解01
- leetcode与python进阶学习总结
- Spring BeanFactoryPostProcessor接口详细使用
- reactjs Fragment的作用
- ubuntu/deepin常用软件清单(都是使用sudo apt-get install安装方式)
- JavaFX弹出窗口和消息对话框代码示例
- nginx指令(alias、set、proxy_pass、rewrite、upstream)、跨域问题、防盗链、缓存、gzip压缩
- Apollo后台配置:命名空间Namespqce配置及代码读取