后面也会持续更新,学到新东西会在其中补充。
建议按顺序食用,欢迎批评或者交流!
缺什么东西欢迎评论!我都会及时修改的!
大部分截图和文章采用该书,谢谢这位大佬的文章,在这里真的很感谢让迷茫的我找到了很好的学习文章。我只是加上了自己的拙见。我只是记录学习没有任何抄袭意思。
MySQL 是怎样运行的:从根儿上理解 MySQL - 小孩子4919 - 掘金小册
先啰嗦一下之前说的表空间
前言
通用链表结构
在写入undo日志
的过程中会使用到多个链表,很多链表都有同样的节点结构。
想定位表空间内的某一个位置的话,只需指定页号以及该位置在指定页号中的页内偏移量即可。
Pre Node Page Number
和Pre Node Offset
的组合就是指向前一个XDES Entry
的指针Next Node Page Number
和Next Node Offset
的组合就是指向后一个XDES Entry
的指针。
整个List Node
占用12个字节
的存储空间。
链表的基节点。这个结构中包含了链表的头节点和尾节点的指针以及这个链表中包含了多少节点的信息。
List Length
表明该链表一共有多少节点First Node Page Number
和First Node Offset
表明该链表的头节点在表空间中的位置。Last Node Page Number
和Last Node Offset
表明该链表的尾节点在表空间中的位置。
整个List Base Node
占用16个字节
的存储空间。
FIL_PAGE_UNDO_LOG页面
表空间其实是由许许多多的页面构成的,页面默认大小为16KB
。
这些页面有不同的类型
比如:
类型为FIL_PAGE_INDEX
的页面用于存储聚簇索引以及二级索引
类型为FIL_PAGE_TYPE_FSP_HDR
的页面用于存储表空间头部信息
类型为FIL_PAGE_UNDO_LOG
的页面是专门用来存储undo日志
FIL_PAGE_UNDO_LOG
简称为Undo页面
,File Header
和File Trailer
是各种页面都有的通用结构。
先就此打住!让我介绍一下其他东西加以理解!
事务回滚的需求
事务需要保证原子性,也就是事务中的操作要么全部完成,要么什么也不做。
但是也有意外出现:
- 情况一:事务执行过程中可能遇到各种错误,比如服务器本身的错误,操作系统错误,甚至是突然断电导致的错误。
- 情况二:程序员可以在事务执行过程中手动输入
ROLLBACK
语句结束当前的事务的执行。
这两种情况都会导致事务执行到一半就结束,但是事务执行过程中可能已经修改了很多东西,为了保证事务的原子性,需要把东西改回原先的样子,这个过程就称之为回滚(英文名:rollback)
每当我们要对一条记录做改动时(这里的改动可以指INSERT
、DELETE
、UPDATE
),都需要留一手 —— 把回滚时所需的东西都给记下来。
insert
语句,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。delete
语句,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。update
语句,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。
为了回滚而记录这种操作称之为撤销日志,英文名为undo log
,也就是undo 日志
由于查询操作(SELECT
)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo日志
。
事务id
给事务分配id
的时机
一个事务可以是一个只读事务,或者是一个读写事务:
- 通过
START TRANSACTION READ ONLY
语句开启一个只读事务。
在只读事务中不可以对普通的表(其他事务也能访问到的表)进行增、删、改操作,但可以对临时表做增、删、改操作。
START TRANSACTION READ WRITE
语句开启一个读写事务,或者使用BEGIN、START TRANSACTION
语句开启的事务默认也算是读写事务。
在读写事务中可以对表执行增删改查操作。
如果某个事务执行过程中对某个表执行了增、删、改操作,那么InnoDB存储引擎
就会给它分配一个独一无二的事务id
,分配方式如下:
- 对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增、删、改操作时才会为这个事务分配一个
事务id
,否则的话是不分配事务id
的。 - 对于读写事务来说,只有在它第一次对某个表(包括用户创建的临时表)执行增、删、改操作时才会为这个事务分配一个
事务id
,否则的话也是不分配事务id
的。
有的时候虽然我们开启了一个读写事务,但是在这个事务中全是查询语句,并没有执行增、删、改的语句,那也就意味着这个事务并不会被分配一个事务id
。
trx_id
隐藏列
对这个聚簇索引记录做改动的语句所在的事务对应的事务id
而已(此处的改动可以是INSERT
、DELETE
、UPDATE
操作)。
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
中分配。
CREATE TABLE undo_demo (id INT NOT NULL,key1 VARCHAR(100),col VARCHAR(100),PRIMARY KEY (id),KEY idx_key1 (key1)
)Engine=InnoDB CHARSET=utf8;
id列
是主键,我们为key1列
建立了一个二级索引,col列
是一个普通的列。
InnoDB
的数据字典,每个表都会被分配一个唯一的table id
。
information_schema.innodb_sys_tables在mysql5.7中有但在mysql9.0没有mysql> SELECT * FROM information_schema.innodb_sys_tables WHERE name = 'test/undo_demo';
+----------+----------------+------+--------+-------+-------------+------------+---------------+------------+
| TABLE_ID | NAME | FLAG | N_COLS | SPACE | FILE_FORMAT | ROW_FORMAT | ZIP_PAGE_SIZE | SPACE_TYPE |
+----------+----------------+------+--------+-------+-------------+------------+---------------+------------+
| 233 | test/undo_demo | 33 | 6 | 253 | Barracuda | Dynamic | 0 | Single |
+----------+----------------+------+--------+-------+-------------+------------+---------------+------------+
1 row in set (0.00 sec)
undo_demo
表对应的table id
为233
INSERT
操作对应的undo
日志
当我们向表中插入一条记录时会有乐观插入和悲观插入的区分,但是不管怎么插入,最终导致的结果就是这条记录被放到了一个数据页中。
如果希望回滚这个插入操作,那么把这条记录删除就好了,也就是说在写对应的undo日志
时,主要是把这条记录的主键信息记上。
类型为TRX_UNDO_INSERT_REC
的undo日志
,它的完整结构如下图所示:
undo no
在一个事务中是从0
开始递增的,也就是说只要事务没提交,每生成一条undo日志
,那么该条日志的undo no
就增1
。- 如果记录中的主键只包含一个列,那么在类型为
TRX_UNDO_INSERT_REC
的undo日志
中只需要把该列占用的存储空间大小和真实值记录下来,如果记录中的主键包含多个列,那么每个列占用的存储空间大小和对应的真实值都需要记录下来(图中的len
就代表列占用的存储空间大小,value
就代表列的真实值)。
向某个表中插入一条记录时,实际上需要向聚簇索引和所有的二级索引都插入一条记录。只需要考虑向聚簇索引插入记录时的情况就好了,因为其实聚簇索引记录和二级索引记录是一一对应的,在回滚插入操作时,只需要知道这条记录的主键信息,然后根据主键信息做对应的删除操作,做删除操作时就会顺带着把所有二级索引中相应的记录也删除掉。
DELETE
操作和UPDATE
操作对应的undo日志
也都是针对聚簇索引记录而言的
BEGIN; # 显式开启一个事务,假设该事务的id为100INSERT INTO undo_demo(id, key1, col) VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');
因为记录的主键只包含一个id列
,所以在对应的undo日志
中只需要将待插入记录的id列
占用的存储空间长度(id列的类型为INT,INT类型占用的存储空间长度为4个字节
)和真实值记录下来。
本例中插入了两条记录,所以会产生两条类型为TRX_UNDO_INSERT_REC
的undo日志
:
- 第一条
undo日志
的undo no
为0
,记录主键占用的存储空间长度为4
,真实值为1
。
- 第二条
undo日志
的undo no
为1
,记录主键占用的存储空间长度为4
,真实值为2
。
为了最大限度的节省
undo日志
占用的存储空间,和redo日志
类似
roll_pointer隐藏列的含义
roll_pointer
占用7个字节
的字段,本质上就是一个指向记录对应的undo日志
的一个指针。
undo_demo
表里插入了2条记录
,每条记录都有与其对应的一条undo日志
。
记录被存储到了类型为FIL_PAGE_INDEX(数据页)
的页面中。
undo日志
被存放到了类型为FIL_PAGE_UNDO_LOG
的页面中。
DELETE
操作对应的undo
日志
插入到页面中的记录会根据记录头信息中的next_record
属性组成一个单向链表,这个链表称之为正常记录链表;被删除的记录其实也会根据记录头信息中的next_record
属性组成一个链表,只不过这个链表中的记录占用的存储空间可以被重新利用,所以也称这个链表为垃圾链表。
Page Header
部分有一个称之为PAGE_FREE
的属性,它指向由被删除记录组成的垃圾链表中的头节点。
正常记录链表中包含了3条正常记录,垃圾链表里包含了2条已删除记录。
在垃圾链表中的这些记录占用的存储空间可以被重新利用。
页面的Page Header
部分的PAGE_FREE
属性的值代表指向垃圾链表头节点的指针。
假设现在准备使用DELETE
语句把正常记录链表中的最后一条记录给删除掉,其实这个删除的过程需要经历两个阶段:
- 阶段一:仅仅将记录的
delete_mask
标识位设置为1
,其他的不做修改(其实会修改记录的trx_id
、roll_pointer
这些隐藏列的值)。这个阶段称之为delete mark
。
正常记录链表中的最后一条记录的delete_mask
值被设置为1
,但是并没有被加入到垃圾链表,也就是此时记录处于一个中间状态。
在删除语句所在的事务提交之前,被删除的记录一直都处于这种所谓的中间状态。
为啥会有这种奇怪的中间状态呢?其实主要是为了实现一个称之为
MVCC
的功能
- 阶段二:当该删除语句所在的事务提交之后,会有专门的线程后来真正的把记录删除掉。所谓真正的删除就是把该记录从正常记录链表中移除,并且加入到垃圾链表中,然后还要调整一些页面的其他信息。
比如页面中的用户记录数量PAGE_N_RECS
、上次插入记录的位置PAGE_LAST_INSERT
、垃圾链表头节点的指针PAGE_FREE
、页面中可重用的字节数量PAGE_GARBAGE
、还有页目录的一些信息等等。
这个阶段称之为purge
。
把阶段二执行完了,这条记录就算是真正的被删除掉了。这条已删除记录占用的存储空间也可以被重新利用了。
将被删除记录加入到垃圾链表时,实际上加入到链表的头节点处,会跟着修改PAGE_FREE
属性的值。
页面的
Page Header
部分有一个PAGE_GARBAGE
属性,该属性记录着当前页面中可重用存储空间占用的总字节数。
每当有已删除记录被加入到垃圾链表后,都会把这个PAGE_GARBAGE
属性的值加上该已删除记录占用的存储空间大小。
PAGE_FREE
指向垃圾链表的头节点,之后每当新插入记录时,首先判断PAGE_FREE
指向的头节点代表的已删除记录占用的存储空间是否足够容纳这条新插入的记录。
如果不可以容纳,就直接向页面中申请新的空间来存储这条记录(并不会尝试遍历整个垃圾链表,找到一个可以容纳新记录的节点)
如果可以容纳,那么直接重用这条已删除记录的存储空间,并且把PAGE_FREE
指向垃圾链表中的下一条已删除记录。
因此出现了问题:
如果新插入的那条记录占用的存储空间大小小于垃圾链表的头节点占用的存储空间大小,那就意味头节点对应的记录占用的存储空间里有一部分空间用不到,这部分空间就被称之为碎片空间。
这些碎片空间占用的存储空间大小会被统计到PAGE_GARBAGE
属性中。
这些碎片空间在整个页面快使用完前并不会被重新利用,不过当页面快满时,如果再插入一条记录,此时页面中并不能分配一条完整记录的空间。
这时候会首先看一看PAGE_GARBAGE
的空间和剩余可利用的空间加起来是不是可以容纳下这条记录,如果可以的话:
InnoDB
会尝试重新组织页内的记录,重新组织的过程就是先开辟一个临时页面,把页面内的记录依次插入一遍,因为依次插入时并不会产生碎片,之后再把临时页面的内容复制到本页面,这样就可以把那些碎片空间都解放出来(很显然重新组织页面内的记录比较耗费性能)。
在删除语句所在的事务提交之前,只会经历阶段一,也就是delete mark
阶段(提交之后我们就不用回滚了,所以只需考虑对删除操作的阶段一做的影响进行回滚)。
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日志
就串成了一个链表。这个链表就称之为版本链
。
- 与类型为
TRX_UNDO_INSERT_REC
的undo日志
不同。
类型为TRX_UNDO_DEL_MARK_REC
的undo日志
还多了一个索引列各列信息的内容
如果某个列被包含在某个索引中,那么它的相关信息就应该被记录到这个索引列各列信息部分,所谓的相关信息包括该列在记录中的位置(用pos
表示)该列占用的存储空间大小(用len
表示),该列实际值(用value
表示)。
索引列各列信息存储的内容实质上就是<pos, len, value>
的一个列表。
这部分信息主要是用在事务提交后,对该中间状态记录做真正删除的阶段二,也就是purge
阶段中使用的。
现在继续在上边那个事务id
为100
的事务中删除一条记录,比如我们把id
为1
的那条记录删除掉:
start transaction;# 插入两条记录
INSERT INTO undo_demo(id, key1, col) VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');delete from undo_demo where id = 1;
delete mark
操作对应的undo日志
的结构就是这样:
- 因为这条
undo日志
是id
为100
的事务中产生的第3
条undo日志
,所以它对应的undo no
就是2
。 - 在对记录做
delete mark
操作时,记录的trx_id
隐藏列的值是100
(也就是说对该记录最近的一次修改就发生在本事务中),所以把100
填入old trx_id
属性中。
然后把记录的roll_pointer
隐藏列的值取出来,填入old roll_pointer
属性中,这样就可以通过old roll_pointer
属性值找到最近一次对该记录做改动时产生的undo日志
。 undo_demo
表中有2
个索引:一个是聚簇索引
,一个是二级索引idx_key1
。
只要是包含在索引中的列,那么这个列在记录中的位置(pos
),占用存储空间大小(len
)和实际值(value
)就需要存储到undo日志
中。
对于主键来说,只包含一个id列
,存储到undo日志
中的相关信息分别是:
pos
:id列
是主键,也就是在记录的第一个列,它对应的pos值
为0
。pos
占用1
个字节来存储。len
:id列
的类型为INT
,占用4
个字节,所以len
的值为4
。len
占用1
个字节来存储。value
:在被删除的记录中id列
的值为1
,也就是value
的值为1
。value
占用4
个字节来存储。
对于id列
来说,最终存储的结果就是<0, 4, 1>
,存储这些信息占用的存储空间大小为1 + 1 + 4 = 6
个字节。
对于idx_key1
来说,只包含一个key1列
,存储到undo日志
中的相关信息分别是:
pos
:key1列
是排在id列
、trx_id列
、roll_pointer列
之后的,它对应的pos
值为3
。pos占用1
个字节来存储。
len
:key1列
的类型为VARCHAR(100)
,使用utf8字符集
,被删除的记录实际存储的内容是AWM
,所以一共占用3
个字节,也就是所以len
的值为3
。len
占用1
个字节来存储。value
:在被删除的记录中key1列
的值为AWM
,也就是value
的值为AWM
。value
占用3
个字节来存储。
所以对于key1列
来说,最终存储的结果就是<3, 3, 'AWM'>
,存储这些信息占用的存储空间大小为1 + 1 + 3 = 5
个字节。
从上边的叙述中可以看到,<0, 4, 1>
和<3, 3, 'AWM'>
共占用11个字节
。然后index_col_info len
本身占用2
个字节,所以加起来一共占用13
个字节,把数字13
就填到了index_col_info len
的属性中。
UPDATE操作对应的undo日志
不更新主键的情况
在不更新主键的情况下,又可以细分为被更新的列占用的存储空间不发生变化和发生变化的情况。
- 就地更新(
in-place update
)
更新记录时,对于被更新的每个列来说,如果更新后的列和更新前的列占用的存储空间都一样大,那么就可以进行就地更新,也就是直接在原记录的基础上修改对应列的值。
是每个列在更新前后占用的存储空间一样大,有任何一个被更新的列更新前比更新后占用的存储空间大,或者更新前比更新后占用的存储空间小都不能进行就地更新。
比方说现在undo_demo
表里还有一条id值
为2
的记录,它的各个列占用的大小如图所示(因为采用utf8字符集
,所以'步枪'
这两个字符
占用6个字节
):
UPDATE undo_demo SET key1 = 'P92', col = '手枪' WHERE id = 2;
col列
从步枪被更新为手枪,前后都占用6个字节
,也就是占用的存储空间大小未改变;
key1列
从M416
被更新为P92
,也就是从4个字节
被更新为3个字节
,这就不满足就地更新需要的条件了,所以不能进行就地更新。
UPDATE undo_demo SET key1 = 'M249', col = '机枪' WHERE id = 2;
由于各个被更新的列在更新前后占用的存储空间是一样大的,所以这样的语句可以执行就地更新。
- 先删除掉旧记录,再插入新记录
在不更新主键的情况下,如果有任何一个被更新的列更新前和更新后占用的存储空间大小不一致,那么就需要先把这条旧的记录从聚簇索引页面中删除掉,然后再根据更新后列的值创建一条新的记录插入到页面中。
这里所说的删除并不是delete mark
操作,而是真正的删除掉,也就是把这条记录从正常记录链表中移除并加入到垃圾链表中,并且修改页面中相应的统计信息(比如PAGE_FREE
、PAGE_GARBAGE
等这些信息)。
这里做真正删除操作的线程并不是DELETE
语句中做purge
操作时使用的,而是由用户线程同步执行真正的删除操作,真正删除之后紧接着就要根据各个列更新后的值创建的新记录插入。
这里如果新创建的记录占用的存储空间大小不超过旧记录占用的空间,那么可以直接重用被加入到垃圾链表中的旧记录所占用的存储空间,否则的话需要在页面中新申请一段空间以供新记录使用,如果本页面内已经没有可用的空间的话,那就需要进行页面分裂操作,然后再插入新记录。
针对UPDATE
不更新主键的情况(包括上边所说的就地更新和先删除旧记录再插入新记录)
类型为TRX_UNDO_UPD_EXIST_REC
的undo日志
,它的完整结构如下:
n_updated
属性表示本条UPDATE
语句执行后将有几个列被更新。
<pos, old_len, old_value>
分别表示被更新列在记录中的位置、更新前该列占用的存储空间大小、更新前该列的真实值。UPDATE
语句中更新的列包含索引列,那么也会添加索引列各列信息这个部分,否则的话是不会添加这个部分的。
事务id
为100
的事务中更新一条记录,比如我们把id
为2
的那条记录更新一下:
BEGIN; # 显式开启一个事务,假设该事务的id为100# 插入两条记录
INSERT INTO undo_demo(id, key1, col) VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');# 删除一条记录
DELETE FROM undo_demo WHERE id = 1; # 更新一条记录
UPDATE undo_demoSET key1 = 'M249', col = '机枪'WHERE id = 2;
这个UPDATE
语句更新的列大小都没有改动,所以可以采用就地更新的方式来执行
在真正改动页面记录时,会先记录一条类型为TRX_UNDO_UPD_EXIST_REC
的undo日志
- 因为这条
undo日志
是id
为100
的事务中产生的第4
条undo日志
,所以它对应的undo no
就是3
。 - 这条日志的
roll_pointer
指向undo no
为1
的那条日志,也就是插入主键值为2
的记录时产生的那条undo日志
,也就是最近一次对该记录做改动时产生的undo日志
。 UPDATE
语句中更新了索引列key1
的值,所以需要记录一下索引列各列信息部分,也就是把主键
和key1列
更新前的信息填入。
更新主键的情况
在聚簇索引中,记录是按照主键值的大小连成了一个单向链表的,如果我们更新了某条记录的主键值,意味着这条记录在聚簇索引中的位置将会发生改变。
如果还有非常多的记录的主键值分布在1 ~ 10000
之间的话,那么这两条记录在聚簇索引中就有可能离得非常远,甚至中间隔了好多个页面。
针对UPDATE
语句中更新了记录主键值的这种情况,InnoDB
在聚簇索引中分了两步处理:
- 将旧记录进行
delete mark
操作
这里是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日志
。
TRX_UNDO_UPD_DEL_REC
的undo日志
的类型(待续)
INSERT
操作对应的undo日志
:TRX_UNDO_INSERT_REC
日志
DELETE
操作对应的undo日志
:TRX_UNDO_DEL_MARK_REC
日志
UPDATE
非主键对应的undo日志
:TRX_UNDO_UPD_EXIST_REC
日志
UPDATE
主键对应的undo日志
:TRX_UNDO_DEL_MARK_REC
日志和TRX_UNDO_INSERT_REC
日志
FIL_PAGE_UNDO_LOG页面(二周目)
TRX_UNDO_PAGE_TYPE
:本页面准备存储什么种类的undo日志
。undo日志
,它们可以被分为两个大类:
TRX_UNDO_INSERT
(使用十进制1
表示):类型为TRX_UNDO_INSERT_REC
的undo日志
属于此大类,一般由INSERT
语句产生,或者在UPDATE
语句中有更新主键的情况也会产生此类型的undo日志
。TRX_UNDO_UPDATE
(使用十进制2
表示),除了类型为TRX_UNDO_INSERT_REC
的undo日志
,其他类型的undo日志
都属于这个大类,比如我们前边说的TRX_UNDO_DEL_MARK_REC
、TRX_UNDO_UPD_EXIST_REC
,一般由DELETE
、UPDATE
语句产生的undo日志
属于这个大类。
这个TRX_UNDO_PAGE_TYPE
属性可选的值就是上边的两个,用来标记本页面用于存储哪个大类的undo日志
不同大类的undo日志
不能混着存储,比如一个Undo页面
的TRX_UNDO_PAGE_TYPE
属性值为TRX_UNDO_INSERT
,那么这个页面就只能存储类型为TRX_UNDO_INSERT_REC
的undo日志
,其他类型的undo日志
就不能放到这个页面中
之所以把
undo日志
分成两个大类,是因为类型为TRX_UNDO_INSERT_REC
的undo日志
在事务提交后可以直接删除掉,而其他类型的undo日志
还需要为所谓的MVCC
服务,不能直接删除掉,对它们的处理需要区别对待。
TRX_UNDO_PAGE_START
:表示在当前页面中是从什么位置开始存储undo日志
的,或者说表示第一条undo日志
在本页面中的起始偏移量。TRX_UNDO_PAGE_FREE
:与上边的TRX_UNDO_PAGE_START
对应,表示当前页面中存储的最后一条undo日志
结束时的偏移量,或者说从这个位置开始,可以继续写入新的undo日志
。
向页面中写入了3
条undo日志
,那么TRX_UNDO_PAGE_START
和TRX_UNDO_PAGE_FREE
的示意图就是这样:
当然,在最初一条undo日志
也没写入的情况下,TRX_UNDO_PAGE_START
和TRX_UNDO_PAGE_FREE
的值是相同的。
TRX_UNDO_PAGE_NODE
:代表一个List Node
结构
Undo页面链表
单个事务中的Undo页面链表
因为一个事务可能包含多个语句,而且一个语句可能对若干条记录进行改动,而对每条记录进行改动前,都需要记录1
条或2
条的undo日志
,所以在一个事务执行过程中可能产生很多undo日志
,这些日志可能一个页面放不下,需要放到多个页面中,这些页面就通过TRX_UNDO_PAGE_NODE
属性连成了链表:
第一个Undo页面
称之为first undo page
,其余的Undo页面
称之为normal undo page
。
在一个事务执行过程中,可能混着执行INSERT
、DELETE
、UPDATE
语句,也就意味着会产生不同类型的undo日志
。
同一个Undo页面
要么只存储TRX_UNDO_INSERT
大类的undo日志
,要么只存储TRX_UNDO_UPDATE
大类的undo日志
。
所以在一个事务执行过程中就可能需要2个Undo页面的链表
,一个称之为insert undo链表
,另一个称之为update undo链表
。
另外,对普通表和临时表的记录改动时产生的undo日志
要分别记录。
一个事务中最多有4
个以Undo页面
为节点组成的链表:
当然,并不是在事务一开始就会为这个事务分配这4
个链表,具体分配策略如下:
- 刚刚开启事务时,一个
Undo页面
链表也不分配。 - 当事务执行过程中向普通表中插入记录或者执行更新记录主键的操作之后,就会为其分配一个普通表的
insert undo链表
。 - 当事务执行过程中删除或者更新了普通表中的记录之后,就会为其分配一个普通表的
update undo链表
。 - 当事务执行过程中向临时表中插入记录或者执行更新记录主键的操作之后,就会为其分配一个临时表的
insert undo链表
。 - 当事务执行过程中删除或者更新了临时表中的记录之后,就会为其分配一个临时表的
update undo链表
。
总结:按需分配,啥时候需要啥时候再分配,不需要就不分配。
多个事务中的Undo页面链表
为了尽可能提高undo日志
的写入效率,不同事务执行过程中产生的undo日志
需要被写入到不同的Undo页面链表
中。
比方说现在有事务id
分别为1
、2
的两个事务
,我们分别称之为trx 1
和trx 2
,假设在这两个事务执行
过程中:
trx 1
对普通表做了DELETE
操作,对临时表做了INSERT
和UPDATE
操作。
InnoDB
会为trx 1
分配3个链表
,分别是:
针对普通表的update undo
链表
针对临时表的insert undo
链表
针对临时表的update undo
链表trx 2
对普通表做了INSERT
、UPDATE
和DELETE
操作,没有对临时表做改动。
InnoDB
会为trx 2
分配2个链表
,分别是:
针对普通表的insert undo
链表
针对普通表的update undo
链表。
在trx 1
和trx 2
执行过程中,InnoDB
共需为这两个事务分配5个Undo页面链表
undo日志具体写入过程
段(Segment)的概念
段
是一个逻辑上的概念,本质上是由若干个零散页面和若干个完整的区组成的。
比如一个B+树索引
被划分成两个段
,一个叶子节点段,一个非叶子节点段,这样叶子节点就可以被尽可能的存到一起,非叶子节点被尽可能的存到一起。
每一个段对应一个INODE Entry
结构,这个INODE Entry
结构描述了这个段的各种信息,比如段的ID
,段内的各种链表基节点,零散页面的页号。
为了定位一个INODE Entry
,设计了一个Segment Header
的结构:
整个Segment Header
占用10个字节
大小,各个属性的意思如下:
-
Space ID of the INODE Entry
:INODE Entry
结构所在的表空间ID
。 -
Page Number of the INODE Entry
:INODE Entry
结构所在的页面页号
。 -
Byte Offset of the INODE Entry
:INODE Entry
结构在该页面中的偏移量
。
知道了表空间ID
、页号
、页内偏移量
,就可以唯一定位一个INODE Entry
的地址。
Undo Log Segment Header
每一个Undo页面
链表都对应着一个段,称之为Undo Log Segment
。
链表中的页面都是从这个段里边申请的,所以在Undo页面
链表的第一个页面,也就是之前提到的first undo page
中设计了一个称之为Undo Log Segment Header
的部分
这个部分中包含了该链表对应的段的segment header
信息以及其他的一些关于这个段的信息,所以Undo页面
链表的第一个页面其实长这样:
Undo Log Segment Header
,看一下它的结构:
-
TRX_UNDO_STATE
:本Undo页面
链表处在什么状态。
一个Undo Log Segment
可能处在的状态包括:TRX_UNDO_ACTIVE
:活跃状态,也就是一个活跃的事务正在往这个段里边写入undo日志
。TRX_UNDO_CACHED
:被缓存的状态。处在该状态的Undo页面
链表等待着之后被其他事务重用。TRX_UNDO_TO_FREE
:对于insert undo
链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。TRX_UNDO_TO_PURGE
:对于update undo
链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。TRX_UNDO_PREPARED
:包含处于PREPARE
阶段的事务产生的undo日志
。
-
TRX_UNDO_LAST_LOG
:本Undo页面
链表中最后一个Undo Log Header
的位置。 -
TRX_UNDO_FSEG_HEADER
:本Undo页面
链表对应的段的Segment Header
信息,通过这个信息可以找到该段对应的INODE Entry
。
-
TRX_UNDO_PAGE_LIST
:Undo页面
链表的基节点。
Undo页面
的Undo Page Header
部分有一个12字节
大小的TRX_UNDO_PAGE_NODE
属性,这个属性代表一个List Node
结构。
每一个Undo页面
都包含Undo Page Header
结构,这些页面就可以通过这个属性连成一个链表。
这个TRX_UNDO_PAGE_LIST
属性代表着这个链表的基节点,当然这个基节点只存在于Undo页面
链表的第一个页面,也就是first undo page
中。
Undo Log Header
Undo页面
链表的第一个页面在真正写入undo日志
前,其实都会被填充Undo Page Header
、Undo Log Segment Header
、Undo Log Header
这3个部分。
Undo Log Header
具体的结构如下:
TRX_UNDO_TRX_ID
:生成本组undo日志
的事务id
。TRX_UNDO_TRX_NO
:事务提交后生成的一个需要序号,使用此序号来标记事务的提交顺序(先提交的此序号小,后提交的此序号大)。TRX_UNDO_DEL_MARKS
:标记本组undo日志
中是否包含由于Delete mark
操作产生的undo日志
。TRX_UNDO_LOG_START
:表示本组undo日志
中第一条undo日志
的在页面中的偏移量。TRX_UNDO_XID_EXISTS
:本组undo日志
是否包含XID
信息。TRX_UNDO_DICT_TRANS
:标记本组undo日志
是不是由DDL语句
产生的。TRX_UNDO_TABLE_ID
:如果TRX_UNDO_DICT_TRANS
为真,那么本属性表示DDL语句
操作的表的table id
。TRX_UNDO_NEXT_LOG
:下一组的undo日志
在页面中开始的偏移量。TRX_UNDO_PREV_LOG
:上一组的undo日志
在页面中开始的偏移量。
一个
Undo页面
链表只存储一个事务执行过程中产生的一组undo日志
,但是在某些情况下,可能会在一个事务提交之后,之后开启的事务重复利用这个Undo页面
链表,这样就会导致一个Undo页面
中可能存放多组Undo日志
,TRX_UNDO_NEXT_LOG
和TRX_UNDO_PREV_LOG
就是用来标记下一组和上一组Undo日志
在页面中的偏移量的。
TRX_UNDO_HISTORY_NODE
:一个12字节
的List Node
结构,代表一个称之为History
链表的节点。
总结
对于没有被重用的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日志
的性能,每个事务单独分配相应的Undo页面
链表(最多可能单独分配4个链表
)。
其实大部分事务执行过程中可能只修改了一条或几条记录,针对某个Undo页面
链表只产生了非常少的undo日志
,这些undo日志
可能只占用一丢丢存储空间,这样就很浪费。
一个Undo页面
链表是否可以被重用的条件很简单:
- 该链表中只包含一个
Undo页面
。
如果一个事务执行过程中产生了非常多的undo日志
,那么它可能申请非常多的页面加入到Undo页面
链表中。
在该事务提交后,如果将整个链表中的页面都重用,那就意味着即使新的事务并没有向该Undo页面
链表中写入很多undo日志
,那该链表中也得维护非常多的页面,那些用不到的页面也不能被别的事务所使用,这样就造成了另一种浪费。 - 该
Undo页面
已经使用的空间小于整个页面空间的3/4
。
Undo页面
链表按照存储的undo日志
所属的大类可以被分为insert undo
链表和update undo
链表两种:
insert undo
链表
insert undo链表
中只存储类型为TRX_UNDO_INSERT_REC
的undo日志
,这种类型的undo日志
在事务提交之后就没用了,就可以被清除掉。
所以在某个事务提交后,重用这个事务的insert undo链表
(这个链表中只有一个页面)时,可以直接把之前事务写入的一组undo日志
覆盖掉,从头开始写入新事务的一组undo日志
,如下图所示:
如图所示,假设有一个事务使用的insert undo
链表,到事务提交时,只向insert undo链表
中插入了3条undo日志
,这个insert undo链表
只申请了一个Undo页面
。假设此刻该页面已使用的空间小于整个页面大小的3/4
,那么下一个事务就可以重用这个insert undo链表
(链表中只有一个页面)。假设此时有一个新事务重用了该insert undo
链表,那么可以直接把旧的一组undo日志
覆盖掉,写入一组新的undo日志
。
当然,在重用
Undo页面链表
写入新的一组undo日志
时,不仅会写入新的Undo Log Header
,还会适当调整Undo Page Header、Undo Log Segment Header、Undo Log Header
中的一些属性,比如TRX_UNDO_PAGE_START、TRX_UNDO_PAGE_FREE
等。
update undo
链表
在一个事务提交后,它的update undo链表
中的undo日志
也不能立即删除掉(这些日志用于MVCC
)。所以如果之后的事务想重用update undo链表
时,就不能覆盖之前事务写入的undo日志
。这样就相当于在同一个Undo页面
中写入了多组的undo日志
,效果看起来就是这样:
回滚段的概念
回滚段的概念
一个事务在执行过程中最多可以分配4个Undo页面链表
,在同一时刻不同事务拥有的Undo页面链表
是不一样的,所以在同一时刻系统里其实可以有许许多多个Undo页面链表
存在。
为了更好的管理这些链表,Rollback Segment Header
的页面出现了。
在这个页面中存放了各个Undo页面链表
的frist undo page的页号
,他们把这些页号称之为undo slot
。
每个Undo页面链表
都相当于是一个班,这个链表的first undo page
就相当于这个班的班长,找到了这个班的班长,就可以找到班里的其他同学(其他同学相当于normal undo page
)。有时候学校需要向这些班级传达一下精神,就需要把班长都召集在会议室,这个Rollback Segment Header
就相当于是一个会议室。
Rollback Segment Header
的页面(以默认的16KB
为例):
每一个Rollback Segment Header
页面都对应着一个段,这个段就称为Rollback Segment
,翻译过来就是回滚段。Rollback Segment
里其实只有一个页面
TRX_RSEG_MAX_SIZE
:本Rollback Segment
中管理的所有Undo页面链表
中的Undo页面
数量之和的最大值。
本Rollback Segment
中所有Undo页面链表
中的Undo页面
数量之和不能超过TRX_RSEG_MAX_SIZE
代表的值。
该属性的值默认为无限大,也就是我们想写多少Undo页面
都可以。TRX_RSEG_HISTORY_SIZE
:History链表
占用的页面数量。TRX_RSEG_HISTORY
:History链表
的基节点。TRX_RSEG_FSEG_HEADER
:本Rollback Segment
对应的10字节
大小的Segment Header
结构,通过它可以找到本段对应的INODE Entry
。TRX_RSEG_UNDO_SLOTS
:各个Undo页面链表
的first undo page
的页号集合,也就是undo slot
集合。
一个页号占用4
个字节,对于16KB
大小的页面来说,这个TRX_RSEG_UNDO_SLOTS
部分共存储了1024
个undo slot
,所以共需1024 × 4 = 4096
个字节。
从回滚段中申请Undo页面链表
初始情况下,由于未向任何事务分配任何Undo页面链表
,所以对于一个Rollback Segment Header页面
来说,它的各个undo slot
都被设置成了一个特殊的值:FIL_NULL
(对应的十六进制就是0xFFFFFFFF
),表示该undo slot
不指向任何页面。
开始有事务需要分配Undo页面链表
了,就从回滚段的第一个undo slot
开始,看看该undo slot
的值是不是FIL_NULL
:
- 如果是
FIL_NULL
,那么在表空间中新创建一个段(也就是Undo Log Segment
),然后从段里申请一个页面作为Undo页面链表
的first undo page
,然后把该undo slot
的值设置为刚刚申请的这个页面的页号,这样也就意味着这个undo slot
被分配给了这个事务。
- 如果不是
FIL_NULL
,说明该undo slot
已经指向了一个undo链表
,也就是说这个undo slot
已经被别的事务占用了,那就跳到下一个undo slot
,判断该undo slot
的值是不是FIL_NULL
,重复上边的步骤。
一个Rollback Segment Header
页面中包含1024个undo slot
,如果这1024个undo slot
的值都不为FIL_NULL
,这就意味着这1024个undo slot
都已经被分配给了某个事务,此时由于新事务无法再获得新的Undo页面链表
,就会回滚这个事务并且给用户报错:
Too many active concurrent transactions
用户看到这个错误,可以选择重新执行这个事务(可能重新执行时有别的事务提交了,该事务就可以被分配Undo页面链表
了)。
当一个事务提交时,它所占用的undo slot
有两种命运:
- 如果该
undo slot
指向的Undo页面链表
符合被重用的条件(就是我们上边说的Undo页面链表
只占用一个页面并且已使用空间小于整个页面的3/4)。
该undo slot
就处于被缓存的状态,该Undo页面链表
的TRX_UNDO_STATE
属性(该属性在first undo page
的Undo Log Segment Header
部分)会被设置为TRX_UNDO_CACHED
。
被缓存的undo slot
都会被加入到一个链表,根据对应的Undo页面链表
的类型不同,也会被加入到不同的链表:
-
如果对应的
Undo页面链表
是insert undo链表
,则该undo slot
会被加入insert undo cached链表
。 -
如果对应的
Undo页面链表
是update undo链表
,则该undo slot
会被加入update undo cached链表
。
一个回滚段就对应着上述两个cached链表
,如果有新事务要分配undo slot
时,先从对应的cached链表
中找。如果没有被缓存的undo slot
,才会到回滚段的Rollback Segment Header页面
中再去找。 -
如果该
undo slot
指向的Undo页面链表
不符合被重用的条件,那么针对该undo slot
对应的Undo页面链表类型
不同,也会有不同的处理:- 如果对应的
Undo页面链表
是insert undo链表
,则该Undo页面链表
的TRX_UNDO_STATE
属性会被设置为TRX_UNDO_TO_FREE
,之后该Undo页面链表
对应的段会被释放掉(也就意味着段中的页面可以被挪作他用),然后把该undo slot
的值设置为FIL_NULL
。 - 如果对应的
Undo页面链表
是update undo链表
,则该Undo页面链表
的TRX_UNDO_STATE
属性会被设置为TRX_UNDO_TO_PRUGE
,则会将该undo slot
的值设置为FIL_NULL
,然后将本次事务写入的一组undo日志
放到所谓的History链表
中(需要注意的是,这里并不会将Undo页面链表
对应的段给释放掉,因为这些undo日志
还有用)。
- 如果对应的
多个回滚段
一个事务执行过程中最多分配4个Undo页面链表
,而一个回滚段里只有1024个undo slot
,很显然undo slot
的数量有点少啊。即使假设一个读写事务执行过程中只分配1个Undo页面链表
,那1024个undo slot
也只能支持1024个读写事务
同时执行,再多了就崩溃了。
因此多定义回滚段就行!定义了128
个回滚段,也就相当于有了128 × 1024 = 131072个undo slot
。
假设一个读写事务执行过程中只分配1个Undo页面链表
,那么就可以同时支持131072个读写事务
并发执行。
每个回滚段都对应着一个Rollback Segment Header页面
,有128个回滚段
,自然就要有128个Rollback Segment Header页面
,在系统表空间的第5号页面
的某个区域包含了128个8字节
大小的格子:
每个8字节
的格子的构造就像这样:
如果所示,每个8字节
的格子其实由两部分组成:
4字节
大小的Space ID
,代表一个表空间的ID
。4字节
大小的Page number
,代表一个页号
。
也就是说每个8字节
大小的格子相当于一个指针,指向某个表空间中的某个页面,这些页面就是Rollback Segment Header
。要定位一个Rollback Segment Header
还需要知道对应的表空间ID
,这也就意味着不同的回滚段可能分布在不同的表空间中。
在系统表空间的第5
号页面中存储了128
个Rollback Segment Header
页面地址,每个Rollback Segment Header
就相当于一个回滚段。在Rollback Segment Header
页面中,又包含1024
个undo slot
,每个undo slot
都对应一个Undo页面链表
。
回滚段的分类
把这128个回滚段
给编一下号,最开始的回滚段称之为第0号回滚段
,之后依次递增,最后一个回滚段就称之为第127号回滚段
。这128个回滚段
可以被分成两大类:
- 第
0
号、第33~127
号回滚段属于一类。其中第0号回滚段
必须在系统表空间中(就是说第0号回滚段
对应的Rollback Segment Header页面
必须在系统表空间中),第33~127号回滚段
既可以在系统表空间中,也可以在自己配置的undo表空间
中。
如果一个事务在执行过程中由于对普通表的记录做了改动需要分配Undo页面链表
时,必须从这一类的段中分配相应的undo slot
。 - 第
1~32号回滚段
属于一类。这些回滚段必须在临时表空间(对应着数据目录中的ibtmp1
文件)中。
如果一个事务在执行过程中由于对临时表的记录做了改动需要分配Undo页面链表
时,必须从这一类的段中分配相应的undo slot
。
也就是说如果一个事务在执行过程中既对普通表的记录做了改动,又对临时表的记录做了改动,那么需要为这个记录分配2个回滚段
,再分别到这两个回滚段中分配对应的undo slot
。
待续。。。
为事务分配Undo页面链表详细过程
事务执行过程中分配Undo页面链表
时的完整过程:
- 事务在执行过程中对普通表的记录首次做改动之前,首先会到系统表空间的
第5号页面
中分配一个回滚段(其实就是获取一个Rollback Segment Header
页面的地址)。一旦某个回滚段被分配给了这个事务,那么之后该事务中再对普通表的记录做改动时,就不会重复分配了。
使用传说中的round-robin
(循环使用)方式来分配回滚段。比如当前事务分配了第0号回滚段
,那么下一个事务就要分配第33号回滚段
,下下个事务就要分配第34号回滚段
,简单一点的说就是这些回滚段被轮着分配给不同的事务。 - 在分配到回滚段后,首先看一下这个回滚段的两个
cached链表
有没有已经缓存了的undo slot
,比如如果事务做的是INSERT
操作,就去回滚段对应的insert undo cached
链表中看看有没有缓存的undo slot
;如果事务做的是DELETE
操作,就去回滚段对应的update undo cached
链表中看看有没有缓存的undo slot
。如果有缓存的undo slot
,那么就把这个缓存的undo slot
分配给该事务。 - 如果没有缓存的
undo slot
可供分配,那么就要到Rollback Segment Header
页面中找一个可用的undo slot
分配给当前事务。
从Rollback Segment Header
页面中分配可用的undo slot
的方式我们上边也说过了,就是从第0个undo slot
开始,如果该undo slot
的值为FIL_NULL
,意味着这个undo slot
是空闲的,就把这个undo slot
分配给当前事务,否则查看第1个undo slot
是否满足条件,依次类推,直到最后一个undo slot
。如果这1024个undo slot
都没有值为FIL_NULL
的情况,就直接报错喽 - 找到可用的
undo slot
后,如果该undo slot
是从cached链表
中获取的,那么它对应的Undo Log Segment
已经分配了,否则的话需要重新分配一个Undo Log Segment
,然后从该Undo Log Segment
中申请一个页面作为Undo页面链表
的first undo page
。 - 然后事务就可以把
undo日志
写入到上边申请的Undo页面链表
了!
对临时表的记录做改动的步骤和上述的一样,就不赘述了。不过需要再次强调一次,如果一个事务在执行过程中既对普通表的记录做了改动,又对临时表的记录做了改动,那么需要为这个记录分配2个回滚段
。并发执行的不同事务其实也可以被分配相同的回滚段,只要分配不同的undo slot
就可以了。
回滚段相关配置
配置回滚段数量
系统中一共有128
个回滚段,其实这只是默认值,我们可以通过启动参数innodb_rollback_segments
来配置回滚段的数量,可配置的范围是1~128
。
mysql> show variables like '%innodb_rollback_segments%';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_rollback_segments | 128 |
+--------------------------+-------+
1 row in set (0.02 sec)
但是这个参数并不会影响针对临时表的回滚段数量,针对临时表的回滚段数量一直是32
- 如果把
innodb_rollback_segments
的值设置为1
,那么只会有1
个针对普通表的可用回滚段,但是仍然有32
个针对临时表的可用回滚段。 - 如果把
innodb_rollback_segments
的值设置为2~33
之间的数,效果和将其设置为1
是一样的。 - 如果把
innodb_rollback_segments
设置为大于33
的数,那么针对普通表的可用回滚段数量就是该值减去32
。
配置undo表空间
默认情况下,针对普通表设立的回滚段(第0
号以及第33~127
号回滚段)都是被分配到系统表空间的。其中的第0
号回滚段是一直在系统表空间的,但是第33~127号回滚段
可以通过配置放到自定义的undo表空间
中。但是这种配置只能在系统初始化(创建数据目录时)的时候使用,一旦初始化完成,之后就不能再次更改了。
- 通过
innodb_undo_directory
指定undo表空间
所在的目录,如果没有指定该参数,则默认undo表空间
所在的目录就是数据目录。
mysql> show variables like 'innodb_undo_directory';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_undo_directory | ./ |
+-----------------------+-------+
1 row in set (0.00 sec)
#数据目录
mysql> show variables like '%datadir%';
+---------------+----------------------------+
| Variable_name | Value |
+---------------+----------------------------+
| datadir | /soft/mysql/mysql9.0/data/ |
+---------------+----------------------------+
1 row in set (0.00 sec)
- 通过
innodb_undo_tablespaces
定义undo表空间
的数量。该参数的默认值为0,表明不创建任何undo表空间
。
mysql> show variables like '%innodb_undo_tablespaces%';
+-------------------------+-------+
| Variable_name | Value |
+-------------------------+-------+
| innodb_undo_tablespaces | 2 |
+-------------------------+-------+
1 row in set (0.00 sec)
第33~127
号回滚段可以平均分布到不同的undo表空间
中。
比如我们在系统初始化时指定的
innodb_rollback_segments
为35
,innodb_undo_tablespaces
为2
,这样就会将第33、34
号回滚段分别分布到一个undo表空间
中。
设立undo表空间
的一个好处就是在undo表空间
中的文件大到一定程度时,可以自动的将该undo表空间
截断(truncate
)成一个小文件。而系统表空间的大小只能不断的增大,却不能截断。