事务03之MVCC机制

MVCC 多版本并发控制机制

文章目录

  • MVCC 多版本并发控制机制
    • 一:并发事务的场景
      • 1:读读场景
      • 2:写写场景
      • 3:读写 or 写读场景
    • 二:MVCC机制综述
      • 1:MVCC日常生活的体现
      • 2:多版本并发控制
    • 三:MVCC实现原理剖析(重点,难点)
      • 1:隐藏字段
        • 1.1:隐藏主键 - ROW_ID(6Bytes)
        • 1.2:删除标识 - Deleted_Bit(1Bytes)
        • 1.3:最近更新的事务ID - TRX_ID(6Bytes)
        • 1.4:回滚指针 - ROLL_PTR(7Bytes)
      • 2:undo_log日志
      • 3:readView(核心)
        • 3.1:到底什么是readView
        • 3.2:机制实现原理和可见性算法
        • 3.3:RC和RR的区别

一:并发事务的场景

1:读读场景

读-读场景即是指多个事务/线程在一起读取一个相同的数据,比如事务T1正在读取ID=88的行记录,事务T2也在读取这条记录,两个事务之间是并发执行的。
在这里插入图片描述
对于这种情况而言,不需要做任何操作,因为不改变数据就不会引起任何并发问题

2:写写场景

多个事务之间一起对同一数据进行写操作,比如事务T1对ID=88的行记录做修改操作,事务T2则对这条数据做删除操作

事务T1提交事务后想查询看一下,结果连这条数据都不见了,这也是所谓的脏写问题,也被称为更新覆盖问题

对于这个问题在所有数据库、所有隔离级别中都是零容忍的存在,最低的隔离级别也要解决这个问题。【写互斥锁就可以解决脏写问题】

在这里插入图片描述

3:读写 or 写读场景

读写 -> 先读后写

在这里插入图片描述

写读,先写后读

在这里插入图片描述
并发事务中同时存在读、写两类操作时,这是最容易出问题的场景,脏读、不可重复读、幻读都出自于这种场景中

当有一个事务在做写操作时,读的事务中就有可能出现这一系列问题,因此数据库才会引入各种机制解决。

二:MVCC机制综述

MVCC机制的全称为Multi-Version Concurrency Control,即多版本并发控制技术,主要是为了提升数据库并发性能而设计的

其中采用更好的方式处理了读-写并发冲突,做到即使有读写冲突时,也可以不加锁解决,从而确保了任何时刻的读操作都是非阻塞的。

1:MVCC日常生活的体现

文章的发布和审核

假设我发布了一篇关于xxx的文章,发布后有一位观看者比较细心,文中存在两三个错别字,被这人指出来了,因此我去修正错别字后重新发布。

问题来了,对于文章首次发布也好,重新发布也罢,绝对要等审核通过后才会正式发布的,那我修正文章后重新发布,文章又会进入「审核中」这个状态

此时对于其他正在看、准备看的人来说,文章是不是就不见了?毕竟文章还在审核,因此对这个业务需求又该如何实现呢?多版本!

也就是说,对于首次发布后通过审核的文章,在后续重新发布审核时,用户可以看到更新前的文章,也就是看到老版本的文章,当更新后的文章审核通过后,再使用新版本的文章代替老版本的文章即可。

这样就能做到新老版本的兼容,也能够确保文章修正时,其他正在阅读的小伙伴不会受影响,而MySQL-MVCC机制的思想也大致相同。
在这里插入图片描述

2:多版本并发控制

MySQL中的多版本并发控制,也和上面给出的例子类似

回想一下,脏读、不可重复读、幻读问题都是由于多个事务并发读写导致的,但这些问题都是基于最新版本的数据并发操作才会出现

如果读、写的事务操作的不是同一个版本呢?这样就可以做到互不影响

⚠️ MySQL中仅在RC、RR才会使用MVCC机制

  • 如果是RU允许存在脏读问题、允许一个事务读取另一个事务未提交的数据,那自然可以直接读最新版本的数据,因此无需MVCC介入。

  • 如果是Serializable串行化级别,因为会将所有的并发事务串行化处理,也就是不论事务是读操作,亦或是写操作,都会被排好队一个个执行,这都不存在所谓的多线程并发问题了,自然也无需MVCC介入。

🖊 MVCC机制在MySQL中,仅有InnoDB引擎支持,而在该引擎中,MVCC机制只对RC、RR两个隔离级别下的事务生效。

当然,RC、RR两个不同的隔离级别中,MVCC的实现也存在些许差异

三:MVCC实现原理剖析(重点,难点)

MVCC由三个部分构成:隐藏字段,undoLog, readview

1:隐藏字段

通常而言,当你基于InnoDB引擎建立一张表后,MySQL除开会构建你显式声明的字段外,通常还会构建一些InnoDB引擎的隐藏字段

在InnoDB引擎中主要有DB_ROW_IDDB_Deleted_BitDB_TRX_IDDB_ROLL_PTR这四个隐藏字段

在这里插入图片描述

1.1:隐藏主键 - ROW_ID(6Bytes)

对于InnoDB引擎的表而言,由于其表数据是按照聚簇索引的格式存储,因此通常都会选择主键作为聚簇索引列,然后基于主键字段构建索引树

但如若表中未定义主键,则会选择一个具备唯一非空属性的字段,作为聚簇索引的字段来构建树。

而当两者都不存在时,InnoDB就会隐式定义一个顺序递增的列ROW_ID来作为聚簇索引列。

因此要牢记一点,如果你选择的引擎是InnoDB,就算你的表中未定义主键、索引,其实默认也会存在一个聚簇索引

只不过这个索引在上层无法使用,仅提供给InnoDB构建树结构存储表数据。

1.2:删除标识 - Deleted_Bit(1Bytes)

对于一条delete语句而言,当执行后并不会立马删除表的数据,而是将这条数据的Deleted_Bit删除标识改为1/true

后续的查询SQL检索数据时,如果检索到了这条数据,但看到隐藏字段Deleted_Bit=1时,就知道该数据已经被其他事务delete了,因此不会将这条数据纳入结果集。

设计Deleted_Bit这个隐藏字段的好处是什么呢?

主要是能够有利于聚簇索引

比如当一个事务中删除一条数据后,后续又执行了回滚操作,假设此时是真正的删除了表数据,会发生什么情况呢?

  • ①删除表数据时,有可能会破坏索引树原本的结构,导致出现叶子节点合并的情况。
  • ②事务回滚时,又需重新插入这条数据,再次插入时又会破坏前面的结构,导致叶子节点分裂。

综上所述,如果执行delete语句就删除真实的表数据,由于事务回滚的问题,就很有可能导致聚簇索引树发生两次结构调整,这其中的开销可想而知;而且先删除,再回滚,最终树又变成了原状,那这两次树的结构调整还是无意义的。

所以,当执行delete语句时,只会改变将隐藏字段中的删除标识改为1/true,如果后续事务出现回滚动作,直接将其标识再改回0/false即可

会不会产生过多的deleted = 1的数据使得磁盘爆满呢?

MySQL中存在purger线程的概念,为了防止“已删除”的数据占用过多的磁盘空间,purger线程会自动清理Deleted_Bit=1/true的行数据。

当然,为了确保清理数据时不会影响MVCC的正常工作,purger线程自身也会维护一个ReadView

如果某条数据的Deleted_Bit=true,并且TRX_ID对purger线程的ReadView可见,那么这条数据一定是可以被安全清除的(即不会影响MVCC工作)。

1.3:最近更新的事务ID - TRX_ID(6Bytes)

MySQL对于每一个创建的事务,都会为其分配一个事务ID[TRX_ID],事务ID同样遵循顺序递增的特性,即后来的事务ID绝对会比之前的ID要大,比如:

此时事务T1准备修改表字段的值,MySQL会为其分配一个事务ID=1,当事务T2准备向表中插入一条数据时,又会为这个事务分配一个ID=2…

但有一个细节点需要记住:MySQL对于所有包含写入SQL的事务,会为其分配一个顺序递增的事务ID,但如果是一条select查询语句,则分配的事务ID=0。

不过对于手动开启的事务,MySQL都会为其分配事务ID,就算这个手动开启的事务中仅有select操作。

表中的隐藏字段TRX_ID,记录的就是最近一次改动当前这条数据的事务ID,这个字段是实现MVCC机制的核心之一。

1.4:回滚指针 - ROLL_PTR(7Bytes)

ROLL_PTR全称为rollback_pointer,也就是回滚指针的意思,这个也是表中每条数据都会存在的一个隐藏字段

当一个事务对一条数据做了改动后,会将旧版本的数据放到Undo-log日志中,而rollback_pointer就是一个地址指针,指向Undo-log日志中旧版本的数据

当需要回滚事务时,就可以通过这个隐藏列,来找到改动之前的旧版本数据,而MVCC机制也利用这点,实现了行数据的多版本。

2:undo_log日志

Undo-log中并不仅仅只存储一条旧版本数据,其实在该日志中会有一个版本链

在这里插入图片描述

从上图中可明显看出:不同的旧版本数据,会以roll_ptr回滚指针作为链接点,然后将所有的旧版本数据组成一个单向链表。

⚠️ 最新的旧版本数据,都会插入到链表头中,而不是追加到链表尾部。[头插法]

上述update语句的详细过程

  1. 对要修改的行数据加上排他锁。
  2. 将原本的旧数据拷贝到Undo-log的rollback Segment区域。
  3. 对表数据上的记录进行修改,修改完成后将隐藏字段中的trx_id改为当前事务ID。
  4. 将隐藏字段中的roll_ptr指向Undo-log中对应的旧数据,并在提交事务后释放锁。

为什么Undo-log日志要设计出版本链呢?

  1. 一方面可以实现事务点回滚(这点回去参考事务篇)
  2. 另一方面则可以实现MVCC机制(这点后面聊)。

移除机制

与删除标识类似,Undo-log移除的工作同样由purger线程负责

purger线程内部也会维护一个ReadView,它会以此作为判断依据,来决定何时移除Undo记录。

3:readView(核心)

思考一个问题:如果T2事务要查询一条行数据,此时这条行数据正在被T1事务写,那也就代表着这条数据可能存在多个旧版本数据

T2事务在查询时,应该读这条数据的哪个版本呢?

在这里插入图片描述
此时就需要用到ReadView,用它来做多版本的并发控制,根据查询的时机来选择一个当前事务可见的旧版本数据读取。

3.1:到底什么是readView

先来说下两个概念,快照读和当前读:

  • 当前读 :在锁定读(使用锁隔离事物)的时候读到的是最新版本的数据

  • 快照读:可重复读下mvcc生效读取的是数据的快照,并不是最新版本的数据(未提交事物的数据)

  • ------- 可以这么区分 ------

  • 快照读:读取的是快照版本。普通的SELECT就是快照读。通过mvcc来进行并发控制的,不用加锁。

  • 当前读:读取的是最新版本。UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE是当前读。

MVCC基于当前MySQL的运行状态生成的快照,也被称之为读视图,即ReadView

在这个快照中记录着当前所有活跃事务的ID(活跃事务是指还在执行的事务,即未结束(提交/回滚)的事务)。

当一个事务启动后,首次执行select操作时,MVCC就会生成一个数据库当前的ReadView,通常而言,一个事务与一个ReadView属于一对一的关系(不同隔离级别下也会存在细微差异),ReadView一般包含四个核心内容:

  • creator_trx_id:代表创建当前这个ReadView的事务ID。
  • trx_ids:表示在生成当前ReadView时,系统内活跃的事务ID列表。
  • up_limit_id:活跃的事务列表中,最小的事务ID。
  • low_limit_id:表示在生成当前ReadView时,系统中要给下一个事务分配的ID值。

⚠️ 值得一提的是low_limit_id,它并不是目前系统中活跃事务的最大ID,因为MySQL的事务ID是按序递增的,因此当启动一个新的事务时,都会为其分配事务ID,而这个low_limit_id则是整个MySQL中,要为下一个事务分配的ID值

在这里插入图片描述
假设目前数据库中共有T1~T5这五个事务,T1、T2、T4还在执行,T3已经回滚,T5已经提交

此时当有一条查询语句执行时,就会利用MVCC机制生成一个ReadView

由于单纯由一条select语句组成的事务并不会分配事务ID,因此默认为0,所以目前这个快照的信息如下:

{"creator_trx_id" : "0","trx_ids" : "[1,2,4]","up_limit_id" : "1","low_limit_id" : "6"
}
3.2:机制实现原理和可见性算法

经过前面得知:

  • 当一个事务尝试改动某条数据时,会将原本表中的旧数据放入Undo-log日志中。
  • 当一个事务尝试查询某条数据时,MVCC会生成一个ReadView快照。

其中Undo-log主要实现数据的多版本,ReadView则主要实现多版本的并发控制

假设现在有两个事务:

# 原始数据如下
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊猫      || 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+# 事务1 trx_id = 1
UPDATE zz_users SET user_name = "竹子" WHERE user_id = 1;
UPDATE zz_users SET user_sex = "男" WHERE user_id = 1;# 事务2 trx_id = 2
SELECT * FROM zz_users WHERE user_id = 1;

目前存在T1、T2两个并发事务,T1目前在修改ID=1的这条数据,而T2则准备查询这条数据,下面就是T2执行的具体的过程:

在这里插入图片描述
在这里插入图片描述
说简单一点,就是首先会去获取表中行数据的隐藏列,然后经过上述一系列判断后,可以得知:目前查询数据的事务到底能不能访问最新版的数据

  • 如果能,就直接拿到表中的数据并返回
  • 不能则去Undo-log日志中获取旧版本的数据返回。

该获取哪个版本的旧数据呢?

如果Undo-log日志中的旧数据存在一个版本链时,此时会首先根据隐藏列roll_ptr找到链表头,然后依次遍历整个列表,从而检索到最合适的一条数据并返回。合适的条件如下:旧版本的数据,其隐藏列trx_id不能在ReadView.trx_ids活跃事务列表中。

因为如果旧版本的数据,其trx_id依旧在ReadView.trx_ids中,就代表着产生这条旧数据的事务还未提交,自然不能读取这个版本的数据

范围查询时,突然出现新增数据怎么办呢?

SELECT * FROM zz_users;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊猫      || 6666     | 2022-08-14 15:22:01 |
|       2 | 竹子      || 1234     | 2022-09-14 16:17:44 |
|       3 | 子竹      || 4321     | 2022-09-16 07:42:21 |
|       4 | 猫熊      || 8888     | 2022-09-27 17:22:59 |
|       9 | 黑竹      || 9999     | 2022-09-28 22:31:44 |
+---------+-----------+----------+----------+---------------------+-- T1事务:查询ID >= 3 的所有用户信息
select * from  zz_users where user_id >= 3;-- T2事务:新增一条 ID = 6 的用户记录
INSERT INTO zz_users VALUES(6,"棕熊","男","7777","2022-10-02 16:21:33");

此时当T1事务查询数据时,突然蹦出来一条ID=6的数据

经过判断之后会发现新增这条数据的事务还在执行,所以要去查询旧版本数据

但此时由于是新增操作,因此roll_ptr=null,即表示没有旧版本数据,此时不会读取最新版的数据

因为如果查询数据的事务不能读取最新版数据,同时又无法从版本链中找到旧数据,那就意味着这条数据对T1事务完全不可见,因此T1的查询结果中不会包含ID=6的这条新增记录。

3.3:RC和RR的区别

在这里插入图片描述

RC工作原理

在这里插入图片描述

RR工作原理

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构-Stack和栈

1.栈 1.1什么是栈 栈是一种特殊的线性表,只允许在固定的一段进行插入和删除操作,进行插入和删除操作的一段称为栈顶,另一端称为栈底。 栈中的数据元素遵顼后进先出LIFO(Last In First Out)的原则,就像一…

langchain基础(二)

一、输出解析器(Output Parser) 作用:(1)让模型按照指定的格式输出; (2)解析模型输出,提取所需的信息 1、逗号分隔列表 CommaSeparatedListOutputParser:…

AI编程:如何编写提示词

这是小卷对AI编程工具学习的第2篇文章,今天讲讲如何编写AI编程的提示词,并结合实际功能需求案例来进行开发 1.编写提示词的技巧 好的提示词应该是:目标清晰明确,具有针对性,能引导模型理解问题 下面是两条提示词的对…

【B站保姆级视频教程:Jetson配置YOLOv11环境(五)Miniconda安装与配置】

B站同步视频教程:https://www.bilibili.com/video/BV1MwFDeyEYC/ 文章目录 0. Anaconda vs Miniconda in Jetson1. 下载Miniconda32. 安装Miniconda33. 换源3.1 conda 换源3.2 pip 换源 4. 创建环境5. 设置默认启动环境 0. Anaconda vs Miniconda in Jetson Jetson…

仿真设计|基于51单片机的无线投票系统仿真

目录 具体实现功能 设计介绍 51单片机简介 资料内容 仿真实现(protues8.7) 程序(Keil5) 全部内容 资料获取 具体实现功能 (1)投票系统分为发送端和接收端。 (2)发送端通过按…

玩转大语言模型——使用langchain和Ollama本地部署大语言模型

系列文章目录 玩转大语言模型——使用langchain和Ollama本地部署大语言模型 玩转大语言模型——ollama导入huggingface下载的模型 玩转大语言模型——langchain调用ollama视觉多模态语言模型 玩转大语言模型——使用GraphRAGOllama构建知识图谱 玩转大语言模型——完美解决Gra…

(动态规划基础 打家劫舍)leetcode 198

已知h2和h1&#xff0c;用已知推出未知 推是求答案&#xff0c;回溯是给答案 这里图片给出dfs暴力&#xff0c;再进行记录答案完成记忆化搜索&#xff0c;再转为dp数组 #include<iostream> #include<vector> #include<algorithm> //nums:2,1,1,2 //dp:2,2,…

origin如何在已经画好的图上修改数据且不改变原图像的画风和格式

例如我现在的.opju文件长这样 现在我换了数据集&#xff0c;我想修改这两个图表里对应的算法里的数据&#xff0c;但是我还想保留这图像现在的形式&#xff0c;可以尝试像下面这样做&#xff1a; 右击第一个图&#xff0c;出现下面&#xff0c;选择Book[sheet1] 选择工作簿 出…

[STM32 - 野火] - - - 固件库学习笔记 - - -十二.基本定时器

一、定时器简介 STM32 中的定时器&#xff08;TIM&#xff0c;Timer&#xff09;是其最重要的外设之一&#xff0c;广泛用于时间管理、事件计数和控制等应用。 1.1 基本功能 定时功能&#xff1a;TIM定时器可以对输入的时钟进行计数&#xff0c;并在计数值达到设定值时触发中…

Python从0到100(八十六):神经网络-ShuffleNet通道混合轻量级网络的深入介绍

前言&#xff1a; 零基础学Python&#xff1a;Python从0到100最新最全教程。 想做这件事情很久了&#xff0c;这次我更新了自己所写过的所有博客&#xff0c;汇集成了Python从0到100&#xff0c;共一百节课&#xff0c;帮助大家一个月时间里从零基础到学习Python基础语法、Pyth…

04树 + 堆 + 优先队列 + 图(D1_树(D1_基本介绍))

目录 一、什么是树&#xff1f; 二、相关术语 根结点 边 叶子结点 兄弟结点 祖先结点 结点的大小 树的层 结点的深度 结点的高度 树的高度 斜树 一、什么是树&#xff1f; 树是一种类似于链表的数据结构&#xff0c;不过链表的结点是以线性方式简单地指向其后继结…

Rust语言进阶之文件处理:std::fs用法实例(九十九)

简介&#xff1a; CSDN博客专家、《Android系统多媒体进阶实战》一书作者 新书发布&#xff1a;《Android系统多媒体进阶实战》&#x1f680; 优质专栏&#xff1a; Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a; 多媒体系统工程师系列【…

flowable expression和json字符串中的双引号内容

前言 最近做项目&#xff0c;发现了一批特殊的数据&#xff0c;即特殊字符"&#xff0c;本身输入双引号也不是什么特殊的字符&#xff0c;毕竟在存储时就是正常字符&#xff0c;只不过在编码的时候需要转义&#xff0c;转义符是\&#xff0c;然而转义符\也是特殊字符&…

学习数据结构(5)单向链表的实现

&#xff08;1&#xff09;头部插入 &#xff08;2&#xff09;尾部删除 &#xff08;3&#xff09;头部删除 &#xff08;4&#xff09;查找 &#xff08;5&#xff09;在指定位置之前插入节点 &#xff08;6&#xff09;在指定位置之后插入节点 &#xff08;7&#xff09;删除…

14-8C++STL的queue容器

一、queue容器 (1)queue容器的简介 queue为队列容器&#xff0c;“先进先出”的容器 (2)queue对象的构造 queue<T>q; queue<int>que Int;//存放一个int的queue容器 queue<string>queString;//存放一个string的queue容器 (3)queue容器的push()与pop()方…

算法基础学习——快排与归并(附带java模版)

快速排序和归并排序是两种速度较快的排序方式&#xff0c;是最应该掌握的两种排序算法&#xff0c; &#xff08;一&#xff09;快速排序&#xff08;不稳定的&#xff09; 基本思想&#xff1a;分治 平均时间复杂度&#xff1a;O(nlogn) / 最慢O(n^2) / 最快O(n) 步骤&…

团体程序设计天梯赛-练习集——L1-028 判断素数

前言 一道10分的题目&#xff0c;相对来说比较简单&#xff0c;思考的时候要仔细且活跃&#xff0c;有时候在写代码的时候一些代码的出现很多余&#xff0c;并且会影响最后的结果 L1-028 判断素数 本题的目标很简单&#xff0c;就是判断一个给定的正整数是否素数。 输入格式…

安卓(android)订餐菜单【Android移动开发基础案例教程(第2版)黑马程序员】

一、实验目的&#xff08;如果代码有错漏&#xff0c;可查看源码&#xff09; 1.掌握Activity生命周的每个方法。 2.掌握Activity的创建、配置、启动和关闭。 3.掌握Intent和IntentFilter的使用。 4.掌握Activity之间的跳转方式、任务栈和四种启动模式。 5.掌握在Activity中添加…

阿里云 - RocketMQ入门

前言 云消息队列 RocketMQ 版产品具备异步通信的优势&#xff0c;主要应用于【异步解耦】、【流量削峰填谷】等场景对于同步链路&#xff0c;需要实时返回调用结果的场景&#xff0c;建议使用RPC调用方案阿里云官网地址RocketMQ官网地址 模型概述 生产者生产消息并发送至服务…

MySQL注入中load_file()函数的使用

前言 在Msql注入中&#xff0c;load_file()函数在获得webshell以及提权过程中起着十分重要的作用&#xff0c;常被用来读取各种配置文件 而load_file函数只有在满足两个条件的情况下才可以使用&#xff1a; 文件权限&#xff1a;chmod ax pathtofile 文件大小&#xff1a;必须…