文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

一文带你了解MySQL之undo日志

2023-08-20 06:41

关注

我们在前边学习事务的时候说过事务需要保证原子性,也就是事务中的操作要么全做,要么全不做。但是有的时候事务会出现一些情况,比如:

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

好比小时候我们和小伙伴们完纸牌。悔牌就是一种非常典型的回滚操作,比如你出两张三纸牌,悔牌对应的操作就是把两张三纸牌在拿出去。数据库中的回滚跟悔牌差不多,你插入了一条记录,回滚操作对应的就是把这条记录删除掉;你更新了一条记录,回滚操作对应的就是把该记录更新为旧值;你删除了一条记录,回滚操作对应的自然就是把该记录再插进去。说的貌似很简单的样子

从上边的描述中我们已经能隐约感觉到,每当我们要对一条记录做改动时(这里的改动可以指INSERTDELETEUPDATE),都需要留一手 —— 把回滚时所需的东西都给记下来。比如说:

数据库这些为了回滚⽽记录的这些东东称之为撤销日志,英文名为undo log,我们称之为undo日志。这里需要注意的一点是,由于查询操作(SELECT)并不会修改任何⽤户记录,所以在查询操作执行时,并不需要记录相应的undo日志。在真实的InnoDB中,undo日志其实并不像我们上边所说的那么简单,不同类型的操作产⽣的undo日志的格式也是不同的,不过先暂时把这些容易让人脑子糊的具体细节放一放,我们先回过头来看看事务id是什么东西

2.1 给事务分配id的时机

我们前边在学习事务简介时说过,一个事务可以是一个只读事务,或者是一个读写事务

如果某个事务执行过程中对某个表执行了操作,那么InnoDB存储引擎就会给它分配一个独一无二的事务id,分配如式如下:

有的时候虽然我们开启了一个读写事务,但是在这个事务中全是查询语句,并没有执行增、删、改的语句,那也就意味着这个事务并不会被分配一个事务id

说了半天,事务id有啥子用?这个先保密哈,后边会一步步的详细唠叨。现在只要知道只有在事务对表中的记录做改动时才会为这个事务分配一个唯一的事务id。

2.2 事务id是怎么生成的

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

这样就可以保证整个系统中分配的事务id值是一个递增的数字。先被分配id的事务得到的是较小的事务id,后被分配id的事务得到的是较大的事务id。

2.3 trx_id隐藏列

我们前边学习InnoDB记录行格式的时候重点强调过:聚簇索引的记录除了会保存完整的用户数据以外,而且还会自动添加名为trx_idroll_pointer的隐藏列,如果用户没有在表中定义主键以及UNIQUE键,还会自动添加一个名为row_id的隐藏列。所以一条记录在页面中的真实结构看起来就是这样的:

在这里插入图片描述

其中的trx_id列其实还蛮好理解的,就是某个对这个聚簇索引记录做改动的语句所在的事务对应的事务id而已(此处的改动可以是INSERTDELETEUPDATE操作)。至于roll_pointer隐藏列我们后边分析~

为了实现事务的原子性,InnoDB存储引擎在实际进行一条记录时,都需要先把对应的undo日志记下来。一般每对一条记录做一次改动,就对应着一条undo日志,但在某些更新记录的操作中,也可能会对应着2条undo日志,这个我们后边会仔细唠叨。一个事务在执行过程中可能新增删除更新若干条记录,也就是说需要记录很多条对应的undo日志,这些undo日志会被从0开始编号,也就是说根据生成的顺序分别被称为第0号undo日志、第1号undo日志、…、第n号undo日志等,这个编号也被称之为undo no

这些undo日志是被记录到类型为FIL_PAGE_UNDO_LOG(对应的十六进制是0x0002,忘记了页面类型是个啥的同学需要回过头再看看前边的章节)的页面中。这些页面可以从系统表空间中分配,也可以从一种专门存放undo日志的表空间,也就是所谓的undo tablespace中分配。不过关于如何分配存储undo日志的页面这个事情我们稍后再说,现在先来看看不同操作都会产生什么样子的undo日志吧~ 为了故事的顺利发展,我们先来创建一个名为demo18的表:

mysql> CREATE TABLE demo18 (    id INT NOT NULL,    key1 VARCHAR(100),    col VARCHAR(100),    PRIMARY KEY (id),    KEY idx_key1 (key1)    )Engine=InnoDB CHARSET=utf8;Query OK, 0 rows affected, 1 warning (0.06 sec)

这个表中有3个列,其中id列是主键,我们为key1列建立了一个二级索引,col列是一个普通的列。我们前边介绍InnoDB的数据字典时说过,每个表都会被分配一个唯一的table id,我们可以通过系统数据库information_schema中的innodb_tables表来查看某个表对应的table id是什么,现在我们查看一下demo18对应的table id是多少:

mysql> SELECT * FROM information_schema.innodb_tables WHERE name = 'testdb/demo18';+----------+---------------+------+--------+-------+------------+---------------+------------+--------------+--------------------+| TABLE_ID | NAME          | FLAG | N_COLS | SPACE | ROW_FORMAT | ZIP_PAGE_SIZE | SPACE_TYPE | INSTANT_COLS | TOTAL_ROW_VERSIONS |+----------+---------------+------+--------+-------+------------+---------------+------------+--------------+--------------------+|     1128 | testdb/demo18 |   33 |      6 |    66 | Dynamic    |             0 | Single     |            0 |                  0 |+----------+---------------+------+--------+-------+------------+---------------+------------+--------------+--------------------+1 row in set (0.00 sec)

从查询结果可以看出,demo18表对应的table id1128,先把这个值记住,我们后边有用

3.1 INSERT操作对应的undo日志

我们前边说过,当我们向表中插入一条记录时会有乐观插入悲观插入的区分,但是不管怎么插入,最终导致的结果就是这条记录被放到了一个数据页中。如果希望回滚这个插入操作,那么把这条记录删除就好了,也就是说在写对应的undo日志时,主要是把这条记录的主键信息记上。所以InnoDB设计了一个类型为TRX_UNDO_INSERT_RECundo日志,它的完整结构如下图所示:

在这里插入图片描述
根据示意图我们强调几点:

现在我们向demo18中插入两条记录:

mysql> BEGIN;  # 显式开启一个事务,假设该事务的id为100Query OK, 0 rows affected (0.00 sec)mysql> # 插入两条记录mysql> INSERT INTO demo18(id, key1, col) VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');Query OK, 2 rows affected (0.01 sec)Records: 2  Duplicates: 0  Warnings: 0

因为记录的主键只包含一个id列,所以我们在对应的undo日志中只需要将待插入记录的id列占用的存储空间长度(id列的类型为INT,INT类型占用的存储空间长度为4个字节)和真实值记录下来。本例中插入了两条记录,所以会产生两条类型为TRX_UNDO_INSERT_RECundo日志:

与第一条undo日志对比,undo no主键各列信息有不同。

roll_pointer隐藏列的含义

是时候揭开roll_pointer的真实面纱了,这个占用7个字节的字段其实一点都不神秘,本质上就是一个指向记录对应的undo日志的一个指针。比如说我们上边向demo18表里插入了2条记录,每条记录都有与其对应的一条undo日志。记录被存储到了类型为FIL_PAGE_INDEX的页面中(就是我们前边一直所说的数据页),undo日志被存放到了类型为FIL_PAGE_UNDO_LOG的页面中。效果如图所示:

在这里插入图片描述
从图中也可以更直观的看出来,roll_pointer本质就是一个指针,指向记录对应的undo日志。不过这7个字节的roll_pointer的每一个字节具体的含义我们后边唠叨完如何分配存储undo日志的页面之后再具体说哈~

3.2 DELETE操作对应的undo日志

我们知道插入到页面中的记录会根据记录头信息中的next_record属性组成一个单向链表,我们把这个链表称之为正常记录链表;我们在前边唠叨数据页结构的时候说过,被删除的记录其实也会根据记录头信息中的next_record属性组成一个链表,只不过这个链表中的记录占用的存储空间可以被重新利用,所以也称这个链表为垃圾链表PageHeader部分有一个称之为PAGE_FREE的属性,它指向由被删除记录组成的垃圾链表中的头节点。为了故事的顺利发展,我们先画一个图,假设此刻某个页面中的记录分布情况是这样的(这个不是demo18表中的记录,只是我们随便举的一个例子):

在这里插入图片描述
为了突出主题,在这个简化版的示意图中,我们只把记录的delete_mask标志位展示了出来。从图中可以看出,正常记录链表中包含了3条正常记录,垃圾链表里包含了2条已删除记录,在垃圾链表中的这些记录占用的存储空间可以被重新利用。页面的Page Header部分的PAGE_FREE属性的值代表指向垃圾链表头节点的指针。假设现在我们准备使用DELETE语句把正常记录链表中的最后一条记录给删除掉,其实这个删除的过程需要经历两个阶段:

从上边的描述中我们也可以看出来,在删除语句所在的事务提交之前,只会经历阶段一,也就是delete mark阶段(提交之后我们就不用回滚了,所以只需考虑对删除操作的阶段一做的影响进行回滚)。InnoDB为此设计了一种称之为TRX_UNDO_DEL_MARK_REC类型的undo日志,它的完整结构如下图所示:

在这里插入图片描述额滴个神呐,这个里边的属性也太多了点儿吧~ (其实大部分属性的意思我们上边已经介绍过了) 是的,的确有点多,不过大家千万不要在意,如果记不住千万不要勉强自已,我这里把它们都列出来让大家混个脸熟而已。劳烦大家先克服一下密集恐急症,再抬头大致看一遍上边的这个类型为TRX_UNDO_DEL_MARK_RECundo日志中的属性,特别注意一下这几点:

该介绍的我们介绍完了,现在继续在上边那个事务id100的事务中删除一条记录,比如我们把id1的那条记录删除掉:

mysql> DELETE FROM demo18 WHERE id = 1;Query OK, 1 row affected (0.01 sec)

这个delete mark操作对应的undo日志的结构就是这样:
在这里插入图片描述对照着这个图,我们得注意下边几点:

3.3 UPDATE操作对应的undo日志

在执行UPDATE语句时,InnoDB对更新主键不更新主键这两种情况有截然不同的处理如案。

3.3.1 不更新主键的情况

在不更新主键的情况下,又可以细分为被更新的列占用的存储空间不发生变化和发生变化的情况。

就地更新(in-place update)

更新记录时,对于被更新的每个列来说,如果更新后的列和更新前的列占用的存储空间都一样大,那么就可以进行就地更新,也就是直接在原记录的基础上修改对应列的值。再次强调一边,是每个列在更新前后占用的存储空间一样大,有任何一个被更新的列更新前比更新后占用的存储空间大,或者更新前比更新后占用的存储空间小都不能进行就地更新。比如说现在demo18表里还有一条id值为2的记录,它的各个列占用的大小如图所示(因为采用utf8字符集,所以’步枪’这两个字符占用6个字节):

在这里插入图片描述
假如我们有这样的UPDATE语句:

UPDATE demo18 SET key1 = 'P92', col = '手枪' WHERE id = 2;

在这个UPDATE语句中,col列从步枪被更新为手枪,前后都占用6个字节,也就是占用的存储空间大小未改变;key1列从M416被更新为P92,也就是从4个字
节被更新为3个字节,这就不满足就地更新需要的条件了,所以不能进行就地更新。但是如果UPDATE语句长这样:

UPDATE demo18 SET key1 = 'M249', col = '机枪'  WHERE id = 2;

由于各个被更新的列在更新前后占用的存储空间是一样大的,所以这样的语句可以执行就地更新。

先删除掉旧记录,再插入新记录

在不更新主键的情况下,如果有任何一个被更新的列更新前和更新后占用的存储空间大小不一致,那么就需要先把这条旧的记录从聚簇索引页面中删除掉,然后再根据更新后列的值创建一条新的记录插入到页面中。

请注意一下,我们这里所说的删除并不是delete mark操作,而是真正的删除掉,也就是把这条记录从正常记录链表中移除并加入到垃圾链表中,并且修改页面中相应的统计信息(比如PAGE_FREEPAGE_GARBAGE等这些信息)。不过这里做真正删除操作的线程并不是在唠叨DELETE语句中做purge操作时使用的另外专门的线程,而是由用户线程同步执行真正的删除操作,真正删除之后紧接着就要根据各个列更新后的值创建的新记录插入。

这里如果新创建的记录占用的存储空间大小不超过旧记录占用的空间,那么可以直接重用被加入到垃圾链表中的旧记录所占用的存储空间,否则的话需要在页面中新申请一段空间以供新记录使用,如果本页面内已经没有可用的空间的话,那就需要进行页面分裂操作,然后再插入新记录。

针对UPDATE不更新主键的情况(包括上边所说的就地更新和先删除旧记录再插入新记录),InnoDB设计了一种类型为TRX_UNDO_UPD_EXIST_REC的undo日志,它的完整结构如下:

在这里插入图片描述
其实大部分属性和我们介绍过的TRX_UNDO_DEL_MARK_REC类型的undo日志是类似的,不过还是要注意这么几点:

现在继续在上边那个事务id100的事务中更新一条记录,比如我们把id2的那条记录更新一下:

BEGIN;  # 显式开启一个事务,假设该事务的id为100# 插入两条记录INSERT INTO demo18(id, key1, col) VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');   # 删除一条记录   DELETE FROM demo18 WHERE id = 1;# 更新一条记录UPDATE demo18 SET key1 = 'M249', col = '机枪' WHERE id = 2;

这个UPDATE语句更新的列大小都没有改动,所以可以采用就地更新的如式来执行,在真正改动页面记录时,会先记录一条类型为TRX_UNDO_UPD_EXIST_REC的undo日志,长这样:

在这里插入图片描述
对照着这个图我们注意一下这几个地如:

3.3.2 更新主键的情况

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

针对UPDATE语句更新记录主键值的这种情况,在对该记录进行delete mark操作前,会记录一条类型为TRX_UNDO_DEL_MARK_REC的undo日志;之后插入新记录时,会记录一条类型为TRX_UNDO_INSERT_RECundo日志,也就是说每对一条记录的主键值做改动时,会记录2条undo日志。这些日志的格式我们上边都唠叨过了,就不赘述了。

写入undo日志的过程中会使用到多个链表,很多链表都有同样的节点结构,如图所示:

在这里插入图片描述
在某个表空间内,我们可以通过一个页的页号和在页内的偏移量来唯一定位一个节点的位置,这两个信息也就相当于指向这个节点的一个指针。所以:

整个List Node占用12个字节的存储空间。为了更好的管理链表,InnoDB的提出了一个基节点的结构,里边存储了这个链表的头节点尾节点以及链表长度信息,基节点的结构示意图如下:

在这里插入图片描述
其中:

整个List Base Node占用16个字节的存储空间。所以使用List Base NodeList Node这两个结构组成的链表的示意图就是这样:

在这里插入图片描述

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

在这里插入图片描述
类型为FIL_PAGE_UNDO_LOG的页我们就简称为Undo页面。上图中的File HeaderFile Trailer是各种页面都有的通用结构,我们前边已经学习了很多遍了,这里就不赘述了。Undo Page HeaderUndo页面所特有的,我们来看一下它的结构:

在这里插入图片描述
其中各个属性的意思如下:

6.1 单个事务中的Undo页面链表

因为一个事务可能包含多个语句,而且一个语句可能对若干条记录进行改动,而对每条记录进行改动前,都需要记录1条或2条的undo日志,所以在一个事务执行过程中可能产生很多undo日志,这些日志可能一个页面放不下,需要放到多个页面中,这些页面就通过我们上边介绍的TRX_UNDO_PAGE_NODE属性连成了链表:

在这里插入图片描述大家可以看一看上边的图,一边情况下把链表中的第一个Undo页称它为first undo page,因为在first undo page中除了记录Undo Page Header之外,还会记录其他的一些管理信息。其余的Undo页面称之为normal undo page

在一个事务执行过程中,可能混着执行INSERT、DELETEUPDATE语句,也就意味着会产生不同类型的undo日志。但是我们前边又说过,同一个Undo页面要么只存储TRX_UNDO_INSERT大类的undo日志,要么只存储TRX_UNDO_UPDATE大类的undo日志,反正不能混着存,所以在一个事务执行过程中就可能需要2个Undo页面的链表,一个称之为insert undo链表,另一个称之为update undo链表,画个示意图就是这样:

在这里插入图片描述
另外,InnoDB对普通表和临时表的记录改动时产生的undo日志要分别记录(后边会有讲解),所以在一个事务中最多有4个以Undo页面为节点组成的链表:
在这里插入图片描述
当然,并不是在事务一开始就会为这个事务分配这4个链表,而是按需分配,具体分配策略如下:

6.2 多个事务中的Undo页面链表

为了尽可能提高undo日志的写入效率,不同事务执行过程中产生的undo日志需要被写入到不同的Undo页面链表中。比如说现在有事务id分别为1、2的两个事务,我们分别称之为trx 1trx 2,假设在这两个事务执行过程中:

综上所述,在trx 1trx 2执行过程中,InnoDB共需为这两个事务分配5个Undo页面链表,画个图就是这样:

在这里插入图片描述
如果有更多的事务,那就意味着可能会产生更多的Undo页面链表。

7.1 段(Segment)的概念

如果你有认真看过表空间那一章的话,对这个段的概念应该印象深刻,我们当时花了非常大的篇幅来唠叨这个概念。简单讲,这个段是一个逻辑上的概念,本质上是由若干个零散页面和若干个完整的区组成的。比如一个B+树索引被划分成两个段,一个叶子节点段,一个非叶子节点段,这样叶子节点就可以被尽可能的存到一起,非叶子节点被尽可能的存到一起。每一个段对应一个INODE Entry结构,这个INODE Entry结构描述了这个段的各种信息,比如段的ID,段内的各种链表基节点,零散页面的页号有哪些等信息(具体该结构中每个属性的意思大家可以到表空间那一章里再次重温一下)。我们前边也说过,为了定位一个INODE Entry,InnoDB的设计了一个Segment Header的结构:

在这里插入图片描述
整个Segment Header占用10个字节大小,各个属性的意思如下:

知道了表空间ID、页号、页内偏移量,不就可以唯一定位一个INODE Entry的地址了么~

小提士:
这部分关于段的各种概念我们在表空间那一章中都有详细解释,在这里重提一下只是为了唤醒大家沉睡的记忆,如果有任何不清楚的地方可以再次跳回表空间的那一章仔细读一下

7.2 Undo Log Segment Header

InnoDB的规定,每一个Undo页面链表都对应着一个段,称之为Undo Log Segment。也就是说链表中的页面都是从这个段里边申请的,所以他们在Undo页面链表的第一个页面,也就是上边提到的first undo page中设计了一个称之为Undo Log Segment Header的部分,这个部分中包含了该链表对应的段的segment header信息以及其他的一些关于这个段的信息,所以Undo页面链表的第一个页面其实长这样:

在这里插入图片描述
可以看到这个Undo链表的第一个页面比普通页面多了个Undo Log Segment Header,我们来看一下它的结构:

在这里插入图片描述
其中各个属性的意思如下:

Undo Log Header

一个事务在向Undo页面中写入undo日志时的方式是十分简单暴力的,就是直接往里写,写完一条紧接着写另一条,各条undo日志之间是亲密无间的。写完一个Undo页面后,再从段里申请一个新页面,然后把这个页面插入到Undo页面链表中,继续往这个新申请的页面中写。InnoDB的认为同一个事务向一个Undo页面链表中写入的undo日志算是一个组,比方说我们上边介绍的trx 1由于会分配3个Undo页面链表,也就会写入3个组的undo日志;trx 2由于会分配2个Undo页面链表,也就会写入2个组的undo日志。在每写入一组undo日志时,都会在这组undo日志前先记录一下关于这个组的一些属性,InnoDB把存储这些属性的地方称之为Undo Log Header。所以Undo页面链表的第一个页面在真正写入undo日志前,其实都会被填充Undo Page HeaderUndo Log Segment HeaderUndo Log Header这3个部分,如图所示:

在这里插入图片描述
这个Undo Log Header具体的结构如下:

在这里插入图片描述
又是一大堆属性,我们先大致看一下它们都是啥意思:

小结

对于没有被重用的Undo页面链表来说,链表的第一个页面,也就是first undo page在真正写入undo日志前,会填充Undo Page Header、Undo Log Segment Header、Undo Log Header这3个部分,之后才开始正式写入undo日志。对于其他的页面来说,也就是normal undo page在真正写入undo日志前,只会填充Undo Page Header。链表的List Base Node存放到first undo page的Undo Log Segment Header部分List Node信息存放到每一个Undo页面的undo Page Header部分,所以画一个Undo页面链表的示意图就是这样:

在这里插入图片描述

我们前边说为了能提高并发执行的多个事务写入undo日志的性能,InnoDB决定为每个事务单独分配相应的Undo页面链表(最多可能单独分配4个链表)。但是这样也造成了一些问题,比如其实大部分事务执行过程中可能只修改了一条或几条记录,针对某个Undo页面链表只产生了非常少的undo日志,这些undo日志可能只占用一丢丢存储空间,每开启一个事务就新创建一个Undo页面链表(虽然这个链表中只有一个页面)来存储这么一丢丢undo日志岂不是太浪费了么?的确是挺浪费,于是InnoDB决定在事务提交后在某些情况下重用该事务的Undo页面链表。一个Undo页面链表是否可以被重用的条件很简单:

9.1 回滚段的概念

我们现在知道一个事务在执行过程中最多可以分配4个Undo页面链表,在同一时刻不同事务拥有的Undo页面链表是不一样的,所以在同一时刻系统里其实可以有许许多多个Undo页面链表存在。为了更好的管理这些链表,InnoDB又设计了一个称之为Rollback Segment Header的页面,在这个页面中存放了各个Undo页面链表的frist undo page的页号,他们把这些页号称之为undo slot。我们可以这样理解,每个Undo页面链表都相当于是一个班,这个链表的first undo page就相当于这个班的班长,找到了这个班的班长,就可以找到班里的其他同学(其他同学相当于normal undo page)。有时候学校需要向这些班级传达一下精神,就需要把班长都召集在会议室,这个Rollback Segment Header就相当于是一个会议室。

我们看一下这个称之为Rollback Segment Header的页面长啥样(以默认的16KB为例):

在这里插入图片描述
InnoDB规定,每一个Rollback Segment Header页面都对应着一个段,这个段就称为Rollback Segment,也就是回滚段。与我们之前介绍的各种段不同的是,这个Rollback Segment里其实只有一个页面(这可能是InnoDB可能觉得为了某个目的去分配页面的话都得先申请一个段,或者他们觉得虽然目前版本的MySQLRollback Segment里其实只有一个页面,但可能之后的版本里会增加页面也说不定)。

了解了Rollback Segment的含义之后,我们再来看看这个称之为Rollback Segment Header的页面的各个部分的含义都是啥意思:

TRX_RSEG_UNDO_SLOTS:各个Undo页面链表的first undo page的页号集合,也就是undo slot集合。

一个页号占用4个字节,对于16KB大小的页面来说,这个TRX_RSEG_UNDO_SLOTS部分共存储了1024undo slot,所以共需1024 × 4 = 4096个字节

9.2 从回滚段中申请Undo页面链表

初始情况下,由于未向任何事务分配任何Undo页面链表,所以对于一个Rollback Segment Header页面来说,它的各个undo slot都被设置成了一个特殊的值:FIL_NULL(对应的十六进制就是0xFFFFFFFF),表示该undo slot不指向任何页面。

随着时间的流逝,开始有事务需要分配Undo页面链表了,就从回滚段的第一个undo slot开始,看看该undo slot的值是不是FIL_NULL

一个Rollback Segment Header页面中包含1024个undo slot,如果这1024undo slot的值都不为FIL_NULL,这就意味着这1024undo slot都已经名花有主(被分配给了某个事务),此时由于新事务无法再获得新的Undo页面链表,就会回滚这个事务并且给用户报错:

Too many active concurrent transactions

用户看到这个错误,可以选择重新执行这个事务(可能重新执行时有别的事务提交了,该事务就可以被分配Undo页面链表了)。

当一个事务提交时,它所占用的undo slot有两种命运:

9.3 多个回滚段

我们说一个事务执行过程中最多分配4个Undo页面链表,而一个回滚段里只有1024个undo slot,很显然undo slot的数量有点少啊。我们即使假设一个读写事务执行过程中只分配1Undo页面链表,那1024undo slot也只能支持1024个读写事务同时执行,再多了就崩溃了。这就相当于会议室只能容下1024个班长同时开会,如果有几千人同时到会议室开会的话,那后来的那些班长就没地方坐了,只能等待前边的人开完会自己再进去开。

话说在InnoDB的早期发展阶段的确只有一个回滚段,但是InnoDB后来意识到了这个问题,咋解决这问题呢?会议室不够,多盖几个会议室不就得了。所以InnoDB一口气定义了128个回滚段,也就相当于有了128 × 1024 = 131072个undo slot。假设一个读写事务执行过程中只分配1Undo页面链表,那么就可以同时支持131072个读写事务并发执行(这么多事务在一台机器上并发执行,还真没见过呢~)

每个回滚段都对应着一个Rollback Segment Header页面,有128个回滚段,自然就要有128Rollback Segment Header页面,这些页面的地址总得找个地方存一下吧!于是InnoDB在系统表空间的第5号页面的某个区域包含了128个8字节大小的格子:

在这里插入图片描述
每个8字节的格子的构造就像这样:

在这里插入图片描述
如果所示,每个8字节的格子其实由两部分组成:

也就是说每个8字节大小的格子相当于一个指针,指向某个表空间中的某个页面,这些页面就是Rollback Segment Header。这里需要注意的一点事,要定位一个Rollback Segment Header还需要知道对应的表空间ID,这也就意味着不同的回滚段可能分布在不同的表空间中。

所以通过上边的叙述我们可以大致清楚,在系统表空间的第5号页面中存储了128Rollback Segment Header页面地址,每个Rollback Segment Header就相当于一个回滚段。在Rollback Segment Header页面中,又包含1024个undo slot,每个undo slot都对应一个Undo页面链表。我们画个示意图:

在这里插入图片描述
把图一画出来就清爽多了。

9.4 回滚段的分类

我们把这128个回滚段给编一下号,最开始的回滚段称之为第0号回滚段,之后依次递增,最后一个回滚段就称之为第127号回滚段。这128个回滚段可以被分成两大类:

也就是说如果一个事务在执行过程中既对普通表的记录做了改动,又对临时表的记录做了改动,那么需要为这个记录分配2个回滚段,再分别到这两个回滚段中分配对应的undo slot

不知道大家有没有疑惑,为啥要把针对普通表和临时表来划分不同种类的回滚段呢?这个还得从Undo页面本身说起,我们说Undo页面其实是类型为FIL_PAGE_UNDO_LOG的页面的简称,说到底它也是一个普通的页面。我们前边说过,在修改页面之前一定要先把对应的redo日志写上,这样在系统奔溃重启时才能恢复到奔溃前的状态。我们向Undo页面写入undo日志本身也是一个写页面的过程,InnoDB为此还设计了许多种redo日志的类型,比方说MLOG_UNDO_HDR_CREATEMLOG_UNDO_INSERTMLOG_UNDO_INIT等等,也就是说我们对Undo页面做的任何改动都会记录相应类型的redo日志。但是对于临时表来说,因为修改临时表而产生的undo日志只需要在系统运行过程中有效,如果系统奔溃了,那么在重启时也不需要恢复这些undo日志所在的页面,所以在写针对临时表的Undo页面时,并不需要记录相应的redo日志。总结一下针对普通表和临时表划分不同种类的回滚段的原因:在修改针对普通表的回滚段中的Undo页面时,需要记录对应的redo日志,而修改针对临时表的回滚段中的Undo页面时,不需要记录对应的redo日志。

小提士:
如果我们仅仅对普通表的记录做了改动,那么只会为该事务分配针对普通表的回滚段,不分配针对临时表的回滚段。但是如果我们仅仅对临时表的记录做了改动,那么既会为该事务分配针对普通表的回滚段,又会为其分配针对临时表的回滚段(不过分配了回滚段并不会立即分配undo slot,只有在真正需要Undo页面链表时才会去分配回滚段中的undo slot)。

9.5 为事务分配Undo页面链表详细过程

上边说了一大堆的概念,大家应该有一点点的小晕,接下来我们以事务对普通表的记录做改动为例,给大家梳理一下事务执行过程中分配Undo页面链表时的完整过程,

对临时表的记录做改动的步骤和上述的一样,就不赘述了。不过需要再次强调一次,如果一个事务在执行过程中既对普通表的记录做了改动,又对临时表的记录做了改动,那么需要为这个记录分配2个回滚段。并发执行的不同事务其实也可以被分配相同的回滚段,只要分配不同的undo slot就可以了。

9.6 回滚段相关配置

9.6.1 配置回滚段数量

我们前边说系统中一共有128个回滚段,其实这只是默认值,我们可以通过启动参数innodb_rollback_segments来配置回滚段的数量,可配置的范围是1~128。但是这个参数并不会影响针对临时表的回滚段数量,针对临时表的回滚段数量一直是32,也就是说:

9.6.2 配置undo表空间

默认情况下,针对普通表设立的回滚段(第0号以及第33~127号回滚段)都是被分配到系统表空间的。其中的第0号回滚段是一直在系统表空间的,但是第33~127号回滚段可以通过配置放到自定义的undo表空间中。但是这种配置只能在系统初始化(创建数据目录时)的时候使用,一旦初始化完成,之后就不能再次更改了。我们看一下相关启动参数:

小提士:
如果我们在系统初始化的时候指定了创建了undo表空间,那么系统表空间中的第0号回滚段将处于不可用状态。

比如我们在系统初始化时指定的innodb_rollback_segments35innodb_undo_tablespaces2,这样就会将第3334号回滚段分别分布到一个undo表空间中。

设立undo表空间的一个好处就是在undo表空间中的文件大到一定程度时,可以自动的将该undo表空间截断truncate)成一个小文件。而系统表空间的大小只能不断的增大,却不能截断。

来源地址:https://blog.csdn.net/liang921119/article/details/130905213

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-数据库
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯