MySQL 事务的底层原理和 MVCC(二)

7.2. undo 日志

7.2.1. 事务回滚的需求

我们说过事务需要保证原子性,也就是事务中的操作要么全部完成,要么什么也不做。但是偏偏有时候事务执行到一半会出现一些情况,比如:
情况一:事务执行过程中可能遇到各种错误,比如服务器本身的错误,操作系统错误,甚至是突然断电导致的错误。
情况二:程序员可以在事务执行过程中手动输入 ROLLBACK 语句结束当前的事务的执行。

这两种情况都会导致事务执行到一半就结束,但是事务执行过程中可能已经修改了很多东西,为了保证事务的原子性,我们需要把东西改回原先的样子,这个过程就称之为回滚(英文名:rollback),这样就可以造成这个事务看起来什么都没做,所以符合原子性要求。

每当我们要对一条记录做改动时(这里的改动可以指 INSERT、DELETE、UPDATE),都需要把回滚时所需的东西都给记下来。比方说:
你插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉。
你删除了一条记录,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中。
你修改了一条记录,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值。
这些为了回滚而记录的这些东西称之为撤销日志,英文名为 undo log/undo日志。这里需要注意的一点是,由于查询操作(SELECT)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的 undo 日志。

当然,在真实的 InnoDB 中,undo 日志其实并不像我们上边所说的那么简单,不同类型的操作产生的 undo 日志的格式也是不同的。

7.2.2. 事务 id

7.2.2.1. 给事务分配 id 的时机

一个事务可以是一个只读事务,或者是一个读写事务:
我们可以通过 START TRANSACTION READ ONLY 语句开启一个只读事务。
在只读事务中不可以对普通的表(其他事务也能访问到的表)进行增、删、改操作,但可以对用户临时表做增、删、改操作。
我们可以通过 START TRANSACTION READ WRITE 语句开启一个读写事务,或者使用 BEGIN、START TRANSACTION 语句开启的事务默认也算是读写事务。
在读写事务中可以对表执行增删改查操作。
如果某个事务执行过程中对某个表执行了增、删、改操作,那么 InnoDB 存储引擎就会给它分配一个独一无二的事务 id,分配方式如下:
对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增、删、改操作时才会为这个事务分配一个事务 id,否则的话是不分配事务 id 的。
我们前边说过对某个查询语句执行 EXPLAIN 分析它的查询计划时,有时候在Extra 列会看到 Using temporary 的提示,这个表明在执行该查询语句时会用到内部临时表。这个所谓的内部临时表和我们手动用 CREATE TEMPORARY TABLE 创建的用户临时表并不一样,在事务回滚时并不需要把执行 SELECT 语句过程中用到的内部临时表也回滚,在执行 SELECT 语句用到内部临时表时并不会为它分配事务 id。
对于读写事务来说,只有在它第一次对某个表(包括用户创建的临时表)执行增、删、改操作时才会为这个事务分配一个事务 id,否则的话也是不分配事务id 的。
有的时候虽然我们开启了一个读写事务,但是在这个事务中全是查询语句,并没有执行增、删、改的语句,那也就意味着这个事务并不会被分配一个事务 id。
上边描述的事务 id 分配策略是针对 MySQL 5.7 来说的,前边的版本的分配方式可能不同。

7.2.2.2. 事务 id 生成机制

这个事务 id 本质上就是一个数字,它的分配策略和我们前边提到的对隐藏列 row_id(当用户没有为表创建主键和 UNIQUE 键时 InnoDB 自动创建的列)的分配策略大抵相同,具体策略如下:

服务器会在内存中维护一个全局变量,每当需要为某个事务分配一个事务 id时,就会把该变量的值当作事务 id 分配给该事务,并且把该变量自增 1。

每当这个变量的值为 256 的倍数时,就会将该变量的值刷新到系统表空间的页号为 5 的页面中一个称之为 Max Trx ID 的属性处,这个属性占用 8 个字节的存储空间。
当系统下一次重新启动时,会将上边提到的 Max Trx ID 属性加载到内存中,将该值加上 256 之后赋值给我们前边提到的全局变量(因为在上次关机时该全局变量的值可能大于 Max Trx ID 属性值)。
这样就可以保证整个系统中分配的事务 id 值是一个递增的数字。先被分配id 的事务得到的是较小的事务 id,后被分配 id 的事务得到的是较大的事务 id。

7.2.3. trx_id 隐藏列

我们在学习 InnoDB 记录行格式的时候重点强调过:聚簇索引的记录除了会保存完整的用户数据以外,而且还会自动添加名为 trx_id、roll_pointer 的隐藏列,
如果用户没有在表中定义主键以及 UNIQUE 键,还会自动添加一个名为 row_id的隐藏列。
在这里插入图片描述
其中的 trx_id 列就是某个对这个聚簇索引记录做改动的语句所在的事务对应的事务 id 而已(此处的改动可以是 INSERT、DELETE、UPDATE 操作)。至于roll_pointer 隐藏列我们后边分析。

7.2.4. undo 日志的格式

为了实现事务的原子性,InnoDB 存储引擎在实际进行增、删、改一条记录时,都需要先把对应的 undo 日志记下来。一般每对一条记录做一次改动,就对应着一条 undo 日志,但在某些更新记录的操作中,也可能会对应着 2 条 undo日志。
一个事务在执行过程中可能新增、删除、更新若干条记录,也就是说需要记录很多条对应的 undo 日志,这些 undo 日志会被从 0 开始编号,也就是说根据生成的顺序分别被称为第 0 号 undo 日志、第 1 号 undo 日志、…、第 n 号 undo日志等,这个编号也被称之为 undo no。
这些 undo 日志是被记录到类型为 FIL_PAGE_UNDO_LOG 的页面中。这些页面可以从系统表空间中分配,也可以从一种专门存放 undo 日志的表空间,也就是所谓的 undo tablespace 中分配。先来看看不同操作都会产生什么样子的 undo日志。

7.2.4.1. INSERT 操作对应的 undo 日志

当我们向表中插入一条记录时最终导致的结果就是这条记录被放到了一个
数据页中。如果希望回滚这个插入操作,那么把这条记录删除就好了,也就是说在写对应的 undo 日志时,主要是把这条记录的主键信息记上。InnoDB 的设计了一个类型为 TRX_UNDO_INSERT_REC 的 undo 日志。

如果记录中的主键只包含一个列,那么在类型为 TRX_UNDO_INSERT_REC 的undo 日志中只需要把该列占用的存储空间大小和真实值记录下来,如果记录中的主键包含多个列,那么每个列占用的存储空间大小和对应的真实值都需要记录下来。
当我们向某个表中插入一条记录时,实际上需要向聚簇索引和所有的二级索引都插入一条记录。不过记录 undo 日志时,我们只需要考虑向聚簇索引插入记录时的情况就好了,因为其实聚簇索引记录和二级索引记录是一一对应的,我们在回滚插入操作时,只需要知道这条记录的主键信息,然后根据主键信息做对应的删除操作,做删除操作时就会顺带着把所有二级索引中相应的记录也删除掉。
后边说到的 DELETE 操作和 UPDATE 操作对应的 undo 日志也都是针对聚簇索引记录而言的。

roll_pointer 的作用
roll_pointer 本质上就是一个指向记录对应的 undo 日志的一个指针。比方说我们向表里插入了 2 条记录,每条记录都有与其对应的一条 undo 日志。记录被存储到了类型为 FIL_PAGE_INDEX 的页面中(就是我们前边一直所说的数据页),undo 日志被存放到了类型为FIL_PAGE_UNDO_LOG 的页面中。roll_pointer 本质就是一个指针,指向记录对应的 undo 日志。

7.2.4.2. DELETE 操作对应的 undo 日志

我们知道插入到页面中的记录会根据记录头信息中的 next_record 属性组成一个单向链表,我们把这个链表称之为正常记录链表;被删除的记录其实也会根据记录头信息中的 next_record 属性组成一个链表,只不过这个链表中的记录占用的存储空间可以被重新利用,所以也称这个链表为垃圾链表。Page Header 部分有一个称之为 PAGE_FREE 的属性,它指向由被删除记录组成的垃圾链表中的头节点。
假设此刻某个页面中的记录分布情况是这样的
在这里插入图片描述
我们只把记录的 delete_mask 标志位展示了出来。从图中可以看出,正常记录链表中包含了 3 条正常记录,垃圾链表里包含了 2 条已删除记录。页面的 PageHeader 部分的 PAGE_FREE 属性的值代表指向垃圾链表头节点的指针。
假设现在我们准备使用 DELETE 语句把正常记录链表中的最后一条记录给删除掉,其实这个删除的过程需要经历两个阶段:

阶段一:将记录的delete_mask标识位设置为1,这个阶段称之为delete mark。
在这里插入图片描述
可以看到,正常记录链表中的最后一条记录的 delete_mask 值被设置为 1,但是并没有被加入到垃圾链表。也就是此时记录处于一个中间状态。在删除语句所在的事务提交之前,被删除的记录一直都处于这种所谓的中间状态。
为啥会有这种奇怪的中间状态呢?其实主要是为了实现一个称之为 MVCC的功能,稍后再介绍。

阶段二:当该删除语句所在的事务提交之后,会有专门的线程后来真正的把记录删除掉。所谓真正的删除就是把该记录从正常记录链表中移除,并且加入到垃圾链表中,然后还要调整一些页面的其他信息,比如页面中的用户记录数量PAGE_N_RECS、上次插入记录的位置PAGE_LAST_INSERT、垃圾链表头节点的指针 PAGE_FREE、页面中可重用的字节数量 PAGE_GARBAGE、还有页目录的一些信息等等。这个阶段称之为 purge。
把阶段二执行完了,这条记录就算是真正的被删除掉了。这条已删除记录占用的存储空间也可以被重新利用了。
在这里插入图片描述
从上边的描述中我们也可以看出来,在删除语句所在的事务提交之前,只会经历阶段一,也就是 delete mark 阶段(提交之后我们就不用回滚了,所以只需考虑对删除操作的阶段一做的影响进行回滚)。InnoDB 中就会产生一种称之为TRX_UNDO_DEL_MARK_REC 类型的 undo 日志。

版本链
同时,在对一条记录进行 delete mark 操作前,需要把该记录的旧的 trx_id和 roll_pointer 隐藏列的值都给记到对应的 undo 日志中来,就是我们图中显示的old trx_id 和 old roll_pointer 属性。这样有一个好处,那就是可以通过 undo 日志的 old roll_pointer 找到记录在修改之前对应的 undo 日志。比方说在一个事务中,我们先插入了一条记录,然后又执行对该记录的删除操作,这个过程的示意图就是这样:
在这里插入图片描述
从图中可以看出来,执行完delete mark操作后,它对应的undo日志和INSERT操作对应的 undo 日志就串成了一个链表。这个链表就称之为版本链。

7.2.4.3. UPDATE 操作对应的 undo 日志

在执行 UPDATE 语句时,InnoDB 对更新主键和不更新主键这两种情况有截然不同的处理方案。
不更新主键的情况
在不更新主键的情况下,又可以细分为被更新的列占用的存储空间不发生变化和发生变化的情况。
就地更新(in-place update)
更新记录时,对于被更新的每个列来说,如果更新后的列和更新前的列占用的存储空间都一样大,那么就可以进行就地更新,也就是直接在原记录的基础上修改对应列的值。再次强调一边,是每个列在更新前后占用的存储空间一样大,有任何一个被更新的列更新前比更新后占用的存储空间大,或者更新前比更新后占用的存储空间小都不能进行就地更新。
先删除掉旧记录,再插入新记录
在不更新主键的情况下,如果有任何一个被更新的列更新前和更新后占用的存储空间大小不一致,那么就需要先把这条旧的记录从聚簇索引页面中删除掉,然后再根据更新后列的值创建一条新的记录插入到页面中。
请注意一下,我们这里所说的删除并不是 delete mark 操作,而是真正的删除掉,也就是把这条记录从正常记录链表中移除并加入到垃圾链表中,并且修改页面中相应的统计信息(比如 PAGE_FREE、PAGE_GARBAGE 等这些信息)。由用户线程同步执行真正的删除操作,真正删除之后紧接着就要根据各个列更新后的值创建的新记录插入。
这里如果新创建的记录占用的存储空间大小不超过旧记录占用的空间,那么可以直接重用被加入到垃圾链表中的旧记录所占用的存储空间,否则的话需要在页面中新申请一段空间以供新记录使用,如果本页面内已经没有可用的空间的话,那就需要进行页面分裂操作,然后再插入新记录。
针对 UPDATE 不更新主键的情况(包括上边所说的就地更新和先删除旧记录再插入新记录),InnoDB 设计了一种类型为TRX_UNDO_UPD_EXIST_REC 的 undo日志。

更新主键的情况

在聚簇索引中,记录是按照主键值的大小连成了一个单向链表的,如果我们更新了某条记录的主键值,意味着这条记录在聚簇索引中的位置将会发生改变,比如你将记录的主键值从 1 更新为 10000,如果还有非常多的记录的主键值分布在 1 ~ 10000 之间的话,那么这两条记录在聚簇索引中就有可能离得非常远,甚至中间隔了好多个页面。针对 UPDATE 语句中更新了记录主键值的这种情况,InnoDB 在聚簇索引中分了两步处理:

将旧记录进行 delete mark 操作
也就是说在 UPDATE 语句所在的事务提交前,对旧记录只做一个 delete mark操作,在事务提交后才由专门的线程做 purge 操作,把它加入到垃圾链表中。这里一定要和我们上边所说的在不更新记录主键值时,先真正删除旧记录,再插入新记录的方式区分开!
之所以只对旧记录做 delete mark 操作,是因为别的事务同时也可能访问这条记录,如果把它真正的删除加入到垃圾链表后,别的事务就访问不到了。这个功能就是所谓的 MVCC。
创建一条新记录
根据更新后各列的值创建一条新记录,并将其插入到聚簇索引中(需重新定位插入的位置)。
由于更新后的记录主键值发生了改变,所以需要重新从聚簇索引中定位这条记录所在的位置,然后把它插进去。
针对 UPDATE 语句更新记录主键值的这种情况,在对该记录进行 delete mark操作前,会记录一条类型为 TRX_UNDO_DEL_MARK_REC 的 undo 日志;之后插入新记录时,会记录一条类型为 TRX_UNDO_INSERT_REC 的 undo 日志,也就是说每对一条记录的主键值做改动时,会记录 2 条 undo 日志。

7.2.5. FIL_PAGE_UNDO_LOG 页面

我们前边说明表空间的时候说过,表空间其实是由许许多多的页面构成的,页面默认大小为 16KB。这些页面有不同的类型,比如类型为 FIL_PAGE_INDEX 的页面用于存储聚簇索引以及二级索引,类型为 FIL_PAGE_TYPE_FSP_HDR 的页面用于存储表空间头部信息的,还有其他各种类型的页面,其中有一种称之为FIL_PAGE_UNDO_LOG 类型的页面是专门用来存储 undo 日志的。

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

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

相关文章

ORB-SLAM3在windows11下的编译使用

01 写在前面 近期在学习SLAM,想部署一下ORB-SLAM3,但是自己电脑是win11系统,因此就想着在win11上部署一下。但是网上看了一些教程,有一些博客,但是可能不适合我这种情况把,就很纠结。先说下结果&#xff0…

【python基础(三)】操作列表:for循环、正确缩进、切片的使用、元组

文章目录 一. 遍历整个列表1. 在for循环中执行更多操作2. 在for循环结束后执行一些操作 二. 避免缩进错误三. 创建数值列表1. 使用函数range()2. 使用range()创建数字列表3. 指定步长。4. 对数字列表执行简单的统计计算5. 列表解析 五. 使用列表的一部分-切片1. 切片2. 遍历切片…

【并发编程】ThreadLocal详解与原理

📫作者简介:小明Java问道之路,2022年度博客之星全国TOP3,专注于后端、中间件、计算机底层、架构设计演进与稳定性建设优化,文章内容兼具广度、深度、大厂技术方案,对待技术喜欢推理加验证,就职于…

【电路笔记】-电流源

电流源 文章目录 电流源1、概述1.1 理想电流源1.2 实际电流源1.3 连接规则 2、依赖电流2.1 压控电流源2.2 电流控制电流源 3、总结 本文为前面文章 电压源的延续,我们将在本文介绍电流源。 与电压源的情况类似,我们将首先介绍理想电流源的概念&#xff…

MySQL 8.2 Command Line Client打开时一闪而过闪退问题

MySQL8.2安装成功后,发现打开MySQL 8.0 Command Line Client时出现一闪而过,打不开的情况。 解决方案: 1、打开MySQL 8.2 Command Line Client文件位置 2、右键选择属性 3、复制它的目标 4、我复制下来的目标路径是这样的,"…

关于 Docker

关于 Docker 1. 术语Docker Enginedockerd(Docker daemon)containerdOCI (Open Container Initiative)runcDocker shimCRI (Container Runtime Interface)CRI-O 2. 容器启动过程在 Linux 中的实现daemon 的作用 Docker 是个划时代的开源项目,…

[计算机网络实验]头歌 实验二 以太网帧、IP报文分析(含部分分析)

目录 第1关:Wireshark基本使用入门 【实验目的】 【实验环境】 【本地主机、平台虚拟机之间数据传递】 wireshark基本用法】 1、wireshark主界面 2、抓取分组操作 3、Wireshark窗口功能 4、筛选分组操作 【实验操作】 ​编辑 第2关:Ethernet帧…

基于Towers of Binary Fields的succinct arguments

1. 引言 Ulvetanna团队Benjamin E. Diamond和Jim Posen 2023年论文《Succinct Arguments over Towers of Binary Fields》,开源代码见: https://github.com/recmo/binius(Rust Sage)【基于plonky3等库】 在该论文中&#xff1…

Apache POI简介

三十二、Apache POI 32.1 介绍 Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用POI在Java程序中对Miscrosoft Office各种文件进行读写操作。 一般情况下,POI都是用于操作Excel文件。 Apache POI 的应用场…

基于区域划分的GaN HEMT 准物理大信号模型

GaN HEMT器件的大信号等效电路模型分为经验基模型和物理基模型。经验基模型具有较高精度但参数提取困难,特别在GaN HEMT器件工艺不稳定的情况下不易应用。相比之下,物理基模型从器件工作机理出发,参数提取相对方便,且更容易更新和…

火山引擎 ByteHouse 的增强型数据导入技术实践

作为企业数字化建设的必备要素,易用的数据引擎能帮助企业提升数据使用效率,更好提升数据应用价值,夯实数字化建设基础。 数据导入是衡量OLAP引擎性能及易用性的重要标准之一,高效的数据导入能力能够加速数据实时处理和分析的效率。…

Sa-Token 整合Java17和SpringBoot

目录 前言引入项目开启登录认证路由拦截鉴权解决兼容问题总结 前言 之前无意中发现Sa-Token权限认证框架,项目十分好用。 项目地址: https://github.com/dromara/sa-token 官网地址: https://sa-token.cc/doc.html#/start/example 我的个人…

不停的挖掘硬盘的最大潜能

从 NAS 上退休的硬盘被用在了监控的存储上了。 随着硬盘使用寿命的接近尾声,感觉就是从高附加值数据到低附加值数据上。监控数据只会保留那么几个月的时间,很多时候都会被覆盖重新写入。 有人问为什么监控数据不保留几年的,那是因为监控数据…

java_函数式接口

文章目录 一、什么是函数式接口二、四大核心函数式接口三、使用举例 一、什么是函数式接口 如果一个接口只有一个抽象方法,那么该接口就是一个函数式接口函数式接口的实例可以通过 lambda 表达式、方法引用或者构造方法引用来创建如果我们在某个接口上声明了 Funct…

从mysql源码编译出相应的库和可执行文件及搭建mysql服务端

目录 1. 问题的提出 2. 源码下载 3. 升级或安装某些前置软件 3.1. 升级CMake 3.2. 升级gcc、g 4. 安装依赖库 4.1. 安装OpenSSL 4.2. 安装Curses 4.3. 安装pkg-config 5. 编译、安装 6. 编译结果、配置 7. 编译错误处理 7.1. 错误1 7.2. 错误2 8. 搭建mysql数…

VMware三种网络模式

桥接模式 NAT(网络地址转换模式) Host-Only(仅主机模式) 参考: vmware虚拟机三种网络模式 - 知乎 (zhihu.com)

一篇文章搞懂WPF动画的使用技巧

WPF 动画系统提供了丰富的功能,用于为 UI 元素创建流畅的动态效果。动画可以应用于任何可用于渲染的属性,比如位置、颜色、大小等。在 WPF 中,动画是通过更改随时间变化的属性来实现的。 WPF动画基本用法 例如实现如下的动画效果&#xff1…

合并区间问题

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。 示例 1: 输入:intervals [[1,…

Java如何获取泛型类型

泛型(Generic) 泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。Ada、Delphi、Eiffel、Java、C#、F#、Swift 和 Vis…

WPF树形控件TreeView使用介绍

WPF 中的 TreeView 控件用于显示层次结构数据。它是由可展开和可折叠的 TreeViewItem 节点组成的&#xff0c;这些节点可以无限嵌套以表示数据的层次。 TreeView 基本用法 例如实现下图的效果&#xff1a; xaml代码如下&#xff1a; <Window x:Class"TreeView01.Mai…