MySQL多版本并发控制原理(MVCC)

news2024/10/7 14:25:00

在数据库系统中,事务是指由一系列数据库操作组成的一个完整的逻辑过程,事务的基本特性是ACID:

A : Atomicity (原子性)

C: Consistency (一致性)

I: Isolation (隔离性)

D: 持久性(Durability)

由于大部分数据库都是高并行发的,即允许多个事务同时对数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。不同的隔离级别代表了数据库在性能和可靠性之间的取舍。那么MySQL是如何保证事务基本特性ACID中的I(Isolation 隔离性)?答案就是InnoDB的多版本并发控制(Multi-Version Concurrency Control MVCC)。

目录

一、事务隔离级别简介

1.1 读未提交(read uncommitted)

1.2 读已提交(read committed)

1.3 可重复读(repeatable read)

1.4 串行化(Serializable)

二、MVCC的概念

三、MVCC实现原理

3.1 undo日志

3.2 版本链

3.3 Readview(读视图)

3.4 ReadView判断示例

3.5 不同隔离级别下ReadView的生成策略


一、事务隔离级别简介

MVCC主要作用就是实现事务的隔离级别,这里先解释一下事务隔离级别及各中隔离级别存在的问题。

SQL标准规定了4种事务隔离级别,分别是:

  • 读未提交(read ncommitted)
  • 读已提交(read committed)
  • 可重复读(repeatable read)
  • 串行化(serializable)

1.1 读未提交(read uncommitted)

会话发起的select语句可以读取其他会话事务还"未提交"的数据,这种现象我们称为"脏读",脏读会带来很多问题,一般不会采用这种隔离级别。

1.2 读已提交(read committed)

会话发起的select语句可以读取其他会话事务还"已提交"的数据,这种情况读取的数据都是其他会话已经提交的,通常不会有什么大问题,Oracle数据库的默认事务隔离级别就是 read committed。

Read Committed隔离级别会导致两个现象:

  • 不可重复读:在同一个事务运行过程中,前后2个相同的select查询之间,其他会话发起事务"修改了"数据并且提交,这就导致了同一个事务中,相同的select两次查询结果不一样,这种现象即"不可重复读"。
  • 幻读:在同一个事务运行过程中,前后2个相同的select查询之间,其他会话发起事务新增了数据(满足前面select条件)并且提交,这就导致了同一事务中,相同的select两次查询结果记录数不一样(第二次查到了会话新插入的记录),这种现象即"幻读"。

不可重复读与幻读的主要区别在于,不可重复读是数据本身的变化,而幻读是记录数量的变化。

1.3 可重复读(repeatable read)

这个是MySQL InnoDB存储引擎的默认隔离级别,在读已提交的隔离级别基础上解决了"不可重复读"问题,但是依然会有"幻读"的问题。

1.4 串行化(Serializable)

所有事务按照串行的方式运行,避免了并行访问数据,由于效率极低,生产环境一般不会使用这种隔离级别。

最常见到的就是"读已提交"和"可重复读"两个隔离级别,另外两个隔离级由于太极端基本不会使用。

四种隔离级别和脏读、不可重复读、幻读的关系总结如下:

脏读

不可重复读

幻读

读未提交(Read uncommitted)

读已提交(read committed)

可重复读(repeatable read)

串行化(Serializable)

注:表中标红的部分特别解释一下,MySQL的默认隔离级别repeatable read,在标准的ANSI隔离级别定义中, repeatable read解决了"不可重复读"的问题,但是是允许"幻读"的。但是MySQL的实现略有不同,在MySQL的repeatable read隔离级别中,通过间隙锁的方式"基本"解决了幻读问题(并没有完全解决,某些情况下还会出现幻读)。

二、MVCC的概念

多版本并发控制(Multi-Version Concurrency Control MVCC)的主要作用是解决读和写的冲突,实现事务隔离,每次修改记录时都会存储这条记录被修改之前的版本,修改之前的内容会记录在undo日志中。如果此时有用户要读取改记录,就可以通过undo日志重构修改前的数据,从而保证读一致性(undo日志还可以用来回滚事务)。

而多次修改的版本之间串联起来就形成了一条版本链,可以保证不同时刻启动的事务可以获得不同版本的数据(MVCC根据控制事务可以看到的数据版本实现了"读已提交"和"可重复读"两个隔离级别)。

三、MVCC实现原理

MySQL在我们插入数据时,实际为每行数据新增了3个隐藏字段:

  • db_trx_id,事务ID,用来记录插入或最后更新该行数据的事务ID。
  • db_roll_pointer,undo日志记录指针,用来指向undo日志中变更记录,通过变更记录可以重构update前的数据。
  • db_row_id,行编号,单调递增的序列,当你没有显式指定主键时,MySQL会利用此字段生成隐藏的主键。

在MVCC的实现中,主要用到db_trx_id和db_roll_pointer两个隐藏字段。

3.1 undo日志

undo log,即回滚日志,它记录了数据更新前的信息,可以用来重构更新前的数据,也是MVCC版本链实现的基础。除了通过MVCC实现一致性读,undo log还会用来回滚事务。

undo log分为两类:

  • insert undo log,只有当事务回滚时才会用到,只要事务提交,立刻就可以丢弃了。
  • update undo log(这里的update包含delete),除了事务回滚,还会被用来保障一致性读,因此只有当没有一致性读需求时才会丢弃。

对于update undo log,如果事务长时间不提交。为了维持版本链,那么undo日志就无法释放,可能导致逐渐累积过大。这就是为什么事务仅仅查询也会影响性能的原因。因此推荐定期的提交事务,释放相关资源。

3.2 版本链

MySQL在更新记录时,每次都在undo日志中记录下更新前的原记录,并通过db_roll_pointer连接起来,当多次更新后,便形成了一个版本链,假设下列场景:

对于一张表MVCC_Chain,有id和value两个字段:

  • 1:00 时执行 insert into mvcc_chain values(1, 'AAA'); commit; 事务ID为1
  • 2:00 时执行 update mvcc_chain set value='BBB' where id=1; commit; 事务ID为5
  • 3:00 时执行 update mvcc_chain set value='CCC' where id=1; commit; 事务ID为10
  • 4:00 时执行 update mvcc_chain set value='DDD' where id=1;  事务ID为15 -- 当前事务,还未提交

事务 ID 是递增分配的,越晚申请的事务ID越大,中间的事务ID假设被其他事务占用了。

那么形成的版本链应该如下:

版本链中共存在4个版本的数据(通过db_roll_pointer串联起来,每次更新都指向上一个版本的数据):

  • 绿色行代表当前存在缓冲区(buffer_pool)中并已经被修改且还未提交的数据。
  • 灰色行代表版本链中通过undo日志重构的历史数据版本。

每行前面的时间代表SQL执行时间,只是为了方便理解。

3.3 Readview(读视图)

有了版本链,MySQL执行select语句时如何判断应该读取链中的哪条数据呢?答案是ReadView,ReadView包含了select语句发起时事务的统计信息,通过这些信息结合版本链中的trx_id,就可以判断该select可以读取哪个版本的数据。

ReadView只有select语句才会生成,其主要包含以下4类信息:

  • creator_trx_id,发起select语句所属事务ID。
  • m_ids,生成ReadView时数据库中活跃的事务ID集合,也就是当前活动中且还未提交的事务ID列表。
  • min_trx_id,生成ReadView时活跃事务ID之中的最小值,即min(m_ids),小于该值的都是已提交事务,而大于则可能提交,也可能未提交,需要结合m_ids判断。
  • max_trx_id,即将分配给下一个事务的 ID 的值,最大事务ID+1

生成ReadView后,会从最新的数据版本开始,通过trx_id,判断数据是否可见,判断步骤如下:

  1. 如果前数据版本的 trx_id = creator_trx_id 说明修改这条数据的事务就是当前事务,数据可见,否则继续判断。
  2. 如果当前数据版本的 trx_id < min_trx_id,说明修改这条数据的事务在当前事务生成 readView 的时候已提交,数据可见,否则继续判断。
  3. 如果当前数据版本的 trx_id >= max_trx_id,说明修改这条数据的事务在当前事务生成 readView 的时候还未启动,数据不可见,继续判断。
  4. 如果当前数据版本的 min_trx_id<= trx_id < max_trx_id,此时 trx_id 若在 m_ids 中(注意这个m_ids的值是离散的,事务只要提交了就不在里面),说明修改这条数据的事务是活跃事务,数据不可见,如果不在m_ids中,则说明事务已提交,数据可见。

如果经过上面4步判断,数据依然不可见,则通过版本链向上追溯一个版本,继续上面4步判断,直到找到可见的数据为止。

可以总结出,ReadView实际上找两类数据:

  • trx_id = creator_trx_id,自己事务更新的数据。
  • trx_d 不在m_ids中的数据(也要小于max_trx_id),即ReadView生成前已提交的数据。

3.4 ReadView判断示例

场景1:假设现在为4:01,我执行了查询select value from mvcc_chain where id=1; 查询事务ID为15,当前数据库中活跃事务为14,15,17(16号事务已提交,所以不在m_ids中),此时生成的ReadView如下,

  • 第1步,当前版本数据的trx_id为15,ReadView的creator_trx_id也是15,两者相等,说明4:00这条update由我自己更新的(同一事务),因此可以直接返回的value值"DDD",判断终止。

场景2:假设现在为4:01,我执行select value from mvcc_chain where id=1; 事务ID为17,生成的ReadView如下:

  • 第1步,当前版本数据的trx_id为15,ReadView的creator_trx_id是17,不是同一个事务,需要继续判断。
  • 第2步,当前版本数据的trx_id为15,trx_id(15)<min_trx_id(14)不满足,该事务在ReadView生成前有可能没提交,需要继续判断。
  • 第3步,当前版本数据的trx_id为15,trx_id(15)>=max_trx_id(18)不满足,那么事务ID肯定在区间[min_trx_id, max_trx_id)中,继续第4步判断。
  • 第4步,当前版本数据的trx_id为15,m_ids为(14,15,17),trx_id in m_ids满足,说明是活跃事务,数据不可见(这里如果trx_id是16就可见了)。

经过上面4步判断,发现trx_id为15的数据对当前会话不可见,那么只能向上追溯一个版本(3:00更新的版本,value值为"CCC",trx_Id为10),继续4步判断:

  • 第5步,当前版本数据的trx_id为10,ReadView的creator_trx_id也是17,数据不可见。
  • 第6步,当前版本数据的trx_id为10,trx_id(10)<min_trx_id(14)满足,说明该版本在select的ReadView生成时已提交,数据可见,返回"CCC",判断终止。

3.5 不同隔离级别下ReadView的生成策略

场景3:假设现在为1:30(数据在1:00插入,且还没有任何更新),我执行了查询select value from mvcc_chain where id=1,查询事务ID为3,生成的ReadView如下:

  • 第1步,当前版本数据的trx_id为1(insert的事务ID),ReadView的creator_trx_id是3,说明不是一个事务,数据不可见。
  • 第2步,当前版本数据的trx_id为1,trx_id(1)<min_trx_id(3)满足,说明ReadView生成时,该事务已提交,数据可见,返回"AAA",判断结束。

假设这个事务3一直没提交,到了4:01又执行了查询select value from mvcc_chain where id=1,此时如果重新生成ReadView(ReadView和场景2基本相同,只是这次加入了1:30开始的一个事务3):

  • 第1步,当前版本数据的trx_id为15,ReadView的creator_trx_id是3,说明不是同一个事务,继续判断。
  • 第2步,当前版本数据的trx_id为15,trx_id(15)<min_trx_id(3)不满足,该事务在ReadView生成前有可能没提交,需要继续判断。
  • 第3步,当前版本数据的trx_id为15,trx_id(15)>=max_trx_id(18)不满足,那么事务ID肯定在区间[min_trx_id, max_trx_id)中,继续第4步判断。
  • 第4步,当前版本数据的trx_id为15,m_ids为(3,14,15,17),trx_id in m_ids满足,说明是活跃事务,数据不可见。

上面4步判断和场景2相同,trx_id为15的数据对当前会话不可见,那么只能向上追溯一个版本(3:00更新的版本,value值为"CCC",trx_Id为10),继续4步判断:

  • 第5步,当前版本数据的trx_id为10,ReadView的creator_trx_id也是3,数据不可见。
  • 第6步,当前版本数据的trx_id为10,trx_id(10)<min_trx_id(3)不满足,说明事务可能没提交,继续判断。
  • 第7步,当前版本数据的trx_id为10,trx_id(10)>=max_trx_id(18)不满足,继续判断。
  • 第8步,当前版本数据的trx_id为10,min_trx_id(3)<=trx_id(10)<max_trx_id(18),并且trx_id(10) 不在活跃事务m_ids(3,14,15,17)中,说明事务已提交,返回"CCC",判断终止。

同一个事务3,在1:30和4:01发出相同的查询,一次返回了"AAA",一次返回了"CCC"(不可重复读现象),其原因就是在4:01的查询时重新生成了ReadView

如果4:01的查询依然拿1:30生成的ReadView来判断,那么它能看到的数据依然是"AAA"。虽然此时数据"BBB"(trx_id=5)和"CCC"(trx_id=10)已提交,但是对于第3步条件trx_id(5/10)>=max_trx_id(5),说明它们是在ReadView生成(1:30)后发起的事务(分别是2:00和3:00发起),数据不可见。

在这个场景中,虽然"BBB"和"CCC" 没有查询能查到,但是为了重构"AAA"的数据,它们也是要一直保存在版本链中的,如果事务3一直挂着不提交(MySQL对事务执行时长没有限制),那么会导致版本链越来越长,undo log占用越来越大。这就是为什么需要定期提交事务的原因。

这两种ReadView生成策略即代表了MySQL的两种隔离级别:

  • 读已提交(Read Committed),每次查询都会生成新的ReadView,因此事务3可以读到后面已提交的数据。
  • 可重复读(Repeatable Read),只有第一次查询生成ReadView,后续查询依然使用此ReadView,因此事务3读不到后面提交的数据。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/778826.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

听GPT 讲K8s源代码--pkg(五)

在 Kubernetes 中&#xff0c;kubelet 是运行在每个节点上的主要组件之一&#xff0c;它负责管理节点上的容器&#xff0c;并与 Kubernetes 控制平面交互以确保容器在集群中按照期望的方式运行。kubelet 的代码位于 Kubernetes 代码库的 pkg/kubelet 目录下。 pkg/kubelet 目录…

档案数字化扫描完成标准有哪些内容?

档案数字化扫描完成标准是指在进行数字化扫描即将纸质文档或图片等非数字化文件转化为数字格式的文件的过程中&#xff0c;要满足一系列严格的要求。 1.扫描速度快。由于档案数量庞大&#xff0c;数字化扫描需要快速高效地进行&#xff0c;因此需要采用高效的扫描设备和软件&am…

函数-嵌入式C语言

函数-嵌入式C语言 值传递 地址传递

基于C语言的科学计算器

完整资料进入【数字空间】查看——baidu搜索"writebug" 一、产品概述 计算器&#xff1a; 它是一个拥有扁平化优雅用户界面的科学计算器&#xff0c;拥有科学计算与基础计算器功能&#xff0c;可以计算是是数学表达式&#xff1a;从一个简单的表达式&#xff0c;如…

postgresql源码学习(58)—— 删除or重命名WAL日志?这是一个问题

最近因为WAL日志重命名踩到大坑&#xff0c;一直很纠结WAL日志在什么情况下会被删除&#xff0c;什么情况下会被重命名&#xff0c;钻研一下这个部分。 一、 准备工作 1. 主要函数调用栈 首先无用WAL日志的清理发生检查点执行时&#xff0c;检查点执行核心函数为CreateCheckPo…

96、Kafka中Zookeeper的作用

Kafka中zk的作用 它是一个分布式协调框架。很好的将消息生产、消息存储、消息消费的过程结合在一起。在典型的Kafka集群中, Kafka通过Zookeeper管理集群配置&#xff0c;选举leader&#xff0c;以及在Consumer Group发生变化时进行rebalance。Producer使用push模式将消息发布到…

PyQt5:使用PyQtWebEngine

1. PyQt 5.13.0 1.1 安装PyQt pip install PyQt55.13.0 -i https://pypi.tuna.tsinghua.edu.cn/simple1.2 安装PyQtWebEngine pip install PyQtWebEngine5.13.0 -i https://pypi.tuna.tsinghua.edu.cn/simplepip list 1.3 测试 python文件 import sys from PyQt5.QtCore imp…

ARM——点灯实验

循环点灯 RCC寄存器使能GPIOE、GPIOF组寄存器 修改GPIOx组寄存器下的值 通过GPIOx_MODER寄存器设置为输出模式通过GPIOx_OTYOER寄存器设置为推挽输出类型通过GPIOx_OSPEEDR寄存器设置为低速输出通过GPIOx_PUPDR寄存器设置为禁止上下拉电阻点灯 通过GPIOx_ODR寄存器设置为高电…

day33哈希表

1.哈希表 常见的哈希表分为三类&#xff0c;数组&#xff0c;set&#xff0c;map&#xff0c;C语言的话是不是只能用数组和 2.例题 题目一&#xff1a; 分析&#xff1a;题目就是判断两个字符串出现的次数是否相同&#xff1b; 1&#xff09;哈希表26个小写字母次数初始化为0&…

K8S初级入门系列之一-概述

一、前言 K8S经过多年的发展&#xff0c;构建了云原生的基石&#xff0c;成为了云原生时代的统治者。我将用三个博客系列全面&#xff0c;循序渐进的介绍K8S相关知识。 初级入门系列&#xff0c;主要针对K8S初学者&#xff0c;以及希望对K8S有所了解的研发人员&#xff0c;重点…

【贪心算法part05】| 435.无重叠区间、763.划分字母区间、56.合并区间

目录 &#x1f388;LeetCode435. 无重叠区间 &#x1f388;LeetCode763.划分字母区间 &#x1f388;LeetCode 56.合并区间 &#x1f388;LeetCode435. 无重叠区间 链接&#xff1a;435.无重叠区间 给定一个区间的集合 intervals &#xff0c;其中 intervals[i] [starti, …

【雕爷学编程】Arduino动手做(55)--DHT11温湿度传感器模块3

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

力扣C++|一题多解之数学题专场(2)

目录 50. Pow(x, n) 60. 排列序列 66. 加一 67. 二进制求和 69. x 的平方根 50. Pow(x, n) 实现 pow(x,n)&#xff0c;即计算 x 的 n 次幂函数&#xff08;即x^n&#xff09;。 示例 1&#xff1a; 输入&#xff1a;x 2.00000, n 10 输出&#xff1a;1024.00000 示例…

【SQL应知应会】表分区(五)• MySQL版

欢迎来到爱书不爱输的程序猿的博客, 本博客致力于知识分享&#xff0c;与更多的人进行学习交流 本文收录于SQL应知应会专栏,本专栏主要用于记录对于数据库的一些学习&#xff0c;有基础也有进阶&#xff0c;有MySQL也有Oracle 分区表 • MySQL版 前言一、分区表1.非分区表2.分区…

day42-servlet下拉查询/单例模式

0目录 1.Servlet实现下拉查询&#xff08;两表&#xff09; 2.单例模式 1.实战 1.1 创建工程&#xff0c;准备环境... 1.2 接口 1.3 重写方法 1.4 servlet 1.5 list.jsp list.jsp详解 2.单例模式 2.1 饿汉模式&#xff1a;在程序加载时直接创建对象&#…

8.4 利用集成运放实现的信号转换电路

在控制、遥控、遥测、近代生物物理和医学等领域&#xff0c;常常需要将模拟信号进行转换&#xff0c;如将信号电压转换成电流&#xff0c;将信号电流转换成电压&#xff0c;将直流信号转换成交流信号&#xff0c;将模拟信号转换成数字信号&#xff0c;等等。 一、电压 - 电流转…

【网络】socket——TCP网络通信 | 日志功能 | 守护进程

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《网络》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 上篇文章中本喵介绍了UDP网络通信的socket代码&#xff0c;今天介绍TCP网络通信的socket代码。 TCP &a…

Flutter系列(2):解决Flutter打包成APP无法访问网络资源

将flutter项目打包成Android后&#xff0c;发现无法访问网络图片&#xff0c;权限不足&#xff0c;没有授权网络权限&#xff0c;解决办法如下&#xff1a; 在android/app/src/main/AndroidManifest.xml中添加如下代码即可 <uses-permission android:name"android.perm…

c语言修炼之指针和数组笔试题解析(1.2)

前言&#xff1a; 书接上回&#xff0c;让我们继续开始今天的学习叭&#xff01;废话不多说&#xff0c;还是字符数组的内容上代码&#xff01; char *p是字符指针&#xff0c;*表示p是个指针&#xff0c;char表示p指向的对象类型是char型&#xff01; char*p"abcdef&q…

第一百一十四天学习记录:C++提高:类模板案例(黑马教学视频)

类模板案例 main.cpp代码&#xff1a; #include "myarray.hpp"void printIntArray(MyArray <int>& arr) {for (int i 0; i < arr.getSize(); i){cout << arr[i] << " ";}cout << endl; }void test01() {MyArray <int&…