前言
事务需要保证原子性,但是总有意外发生。事务执行过程中可能遇到各种错误,或者手动执行ROLLBACK语句。这两种情况都会导致事务执行到一半就中止,但此时事务在执行的过程中已经修改了许多东西。为了保证原子性,必须要回退至原来的状态,这个过程就是回滚。
回滚的大致步骤就是,插入了一条记录,回滚就删除这条记录;更新了一条记录,回滚就将记录的旧值再更新回来;删除了一条记录,回滚就将这条记录再插入进来。
- 在插入一条记录时,至少要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删除就好了;
- 在删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入表中就好了;
- 在修改一条记录时,至少要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。
undo日志是什么
undo日志就是为了回滚而被设计的记载了回滚一个操作所需要的内容。
undo日志的作用是什么
在事务执行过程中,发生程序错误或认为回滚时,将对数据库已经做出的修改全部撤销。
事务id
分配事务id
当某个事务在执行过程中对某个表执行了增删改操作,那么InnoDB存储引擎就会给它分配一个独一无二的事务id。分配方式如下:
- 只读事务:只有它第一次对某个用户创建的临时表执行增删改操作是,才会为这个事务分配一个事务id。
- 读写事务:它第一次对某个表执行增删改查操作时,就会为这个事务分配一个事务id。
说重点,只有在事务对表中的记录进行改动时才会为这个事务分配一个唯一的事务id,如果不为事务分配事务id,那么它的事务id为默认值0.
生成事务id
事务id本质就是一个数字,生成过程如下:
- 服务器会在内存中维护一个全局变量,每当需要为某个事务分配事务id时,就会把这个变量的值当做事务id分配给该事务,并自增1。
- 每当这个变量的值为256的倍数时,就会将该变量的值刷新到系统表空间中页号为5的页面中名为Max Trx ID属性,这个属性占用8字节空间。
- 当系统下一次重启时,会将这个Max Trx ID属性加载到内存中,并将该值加256再复制给分配事务id的这个全局变量。
记录行格式中的trx_id隐藏列
每条聚簇索引记录中都有一个trx_id隐藏列,记载的就是对这个记录进行改动的语句所在的事务对应的事务id。包括INSERT、DELETE、UPDATE操作。
记录行格式中的roll_pointer隐藏列
每条聚簇索引记录中还有一个roll_pointer隐藏列,记载的是该记录对应的undo日志存放地址。
undo日志的格式
为了实现事务的原子性,InnoDB存储引擎在实际进行记录的增删改操作时,都需要先把对应的undo日志记下来。一般每对一条记录进行一次改动,就对应着一条undo日志。一个事务在执行过程中需要记录很多条undo日志,这些undo日志会从0开始,按照生成的顺序进行编号,这个编号叫undo no。
undo日志被记录在类型为FIL_PAGE_UNDO_LOG的页面中,这些页面可以从系统表空间中分配,也可以从一种专门存放undo日志的表空间undo tablespace中分配。不同的操作需要记录的的信息不一样,用来记录这些信息的undo日志类型也随之不一样。
创建一个表:
CREATE TABLE undo_demo (
id INT NOT NULL,
key1 VARCHAR(100),
col VARCHAR(100),
PRIMARY KEY (id),
INDEX idx_key1(key1)
)Engine=InnoDB CHARSET=utf8;
查看表的table id:
SELECT * FROM information_schema.INNODB_SYS_TABLES WHERE name = 'study/undo_demo';
INSERT操作对应的undo日志
INSERT操作对应的undo日志,只要把这条记录的主键信息记上就好了。MySQL设计了一种类型为TRX_UNDO_INSERT_REC的undo日志。
- undo no在一个事务中是从0开始递增的,只要事务没提交,没生成一条undo日志,那么该日志的undo no加1。
- 记录中的主键有多少列,在undo日志中都要把每个列占用的存储空间大小和对应的真实值记录下来。
向表中插入一条记录时,实际上是向聚簇索引和所有的二级索引中都插入一条记录。不过在undo日志中,只需要针对聚簇索引记录来记录一条undo日志就好了。
对于INSERT操作造成的影响,通过DELETE PRIMARY KEY就可以达到回滚的效果。
现在向表中插入两条记录:
BEGIN; # 显式开启事务,假设事务id为100
INSERT INTO undo_demo(id, key1, col) VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');
因为记录的主键只有id这一列,所以undo日志中主键信息只需要记录id列占用的存储空间大小和真实值就好了。这个事务插入了两条记录,产生了两条类型为TRX_UNDO_INSERT_REC的undo日志。
- 第1条undo日志的undo no为0,记录主键占用的存储空间长度为4,真实值为1。
- 第2条undo日志的undo no为1,记录主键占用的存储空间长度为4,真实值为2,
DELETE操作对应的undo日志
插入到页面中记录会根据记录头信息中的next_record组成一个单向链表,当记录被删除后,被删除的记录也会根据记录头信息中的next_record组成一个垃圾链表,这个链表中的记录占用的空间可以被重新利用。Page Header部分中有一个PAGE_FREE属性,表示垃圾链表的头节点地址。
一条DELETE语句的执行过程经历两个阶段:
- 阶段1:将记录的记录头信息中的deleted_flag属性设置为1。这个阶段叫delete mark。
- 阶段2:将该记录从正常记录链表中移除,并加入到垃圾链表中。这个阶段叫purge。
delete mark一条记录不仅是修改记录的deleted_flag,还会修改记录的trx_id、roll_pointer。
purge一条记录也还要调整一些页面的信息,维护用户记录数量PAGE_N_RECS、上次插入记录的位置PAGE_LAST_INSERT、垃圾链表头节点的指针PAGE_FREE、页面中可重用的字节数量PAGE_GARBAGE,以及页目录信息等。
每当新插入记录时,首先判断垃圾链表头节点的已删除记录所占用的存储空间是否足够容纳这条新记录,如果无法容纳,就直接向页面申请新的空间来存储这条新纪录。如果可以容纳,就直接重用这条已删除记录的存储空间,并修改PAGE_FREE指向垃圾链表中下一条已删除记录。
当页快满时,插入新纪录无法在该页中申请到新空间,这时候会看PAGE_GARBAGE的值是否大于新记录所需存储空间大小,如果足够,则将页中的记录进行整理,过程中需要用到临时页面。如果不能满足,那就申请新的页面。
在DELETE语句所在的事务提交之前,被删除的记录一直处于阶段1的状态,只有当事务提交后,才会完成阶段2的操作,这条记录才算是真正地被删除了,所占用的存储空间可以被重新利用。被删除的记录加入到垃圾链表的头节点出,并修改PAGR_FREE为新的头节点地址,PAGE_GARBAGE加上删除记录占用的存储空间大小。
事务提交后,不再需要回滚操作,undo日志就没有存在的意义了。所以只需要记录事务执行过程中的记录就好了。于是针对DELETE所做的delete mark的影响,设计了一种类型为TRX_UNDO_DEL_MARK_REC的undo日志。
- 在最一条记录进行delete mark操作前,需要把该记录中的trx_id和roll_pointer隐藏列的旧值都挤到对应的undo日志中。这么做的目的是通过undo日志的roll_pointer属性找到上一次对该记录进行修改时产生的undo日志。这在事务中形成了一个版本连。若在一个事务中插入一条记录又删除该记录,则这条记录DELETE操作对应的undo日志的roll_pointer指向这条记录的INSERT操作的undo日志。
- 如果某个列被包含在某个索引中,那么它的相关信息就应该记录到这个索引列各列的信息部分。这部分信息主要在事务提交后,用来真正删除记录(从聚簇索引、二级索引中真正的删除记录,也就是purge)。
roll_pointer一共有7个字节,包含的属性如下:
名称 | 大小 | 描述 |
---|---|---|
is_insert | 1bit | 表示指向的undo日志是否属于TRX_UNDO_INSERT大类 |
rseg_id | 7bit | 表示指向的undo日志的回滚段编号(128个回滚段之一) |
page number | 32bit | 表示指向的undo日志所在页面的页号 |
offset | 16bit | 表示指向的undo日志在页面中的偏移量 |
删除一条记录:
BEGIN; # 显式开启事务,假设事务id为100
INSERT INTO undo_demo(id, key1, col) VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');
DELETE FROM undo_demo WHERE id = 1;
- 同一个事务中产生的undo日志的trx_id相同,undo no按照产生顺序递增。
- 对记录执行delete mark操作时,记录的trx_id设置为本次事务id,然后把记录的roll_pointer值(对应记录上一次修改时产生的undo日志)取出来,赋值给undo日志的roll_pointer。
UPDATE操作对应的undo日志
在执行UPDATE语句时,需要更新主键和不更新主键两种情况有不同的处理方式。
不更新主键时
在不更新主键情况下,又分为被更新的列占用的存储空间不发生变化和发生变化两种情况。
就地更新
在更新记录时,更新后的列与更新前的列占用的存储空间一样大,可以进行就地更新,也就是直接在原记录的基础上修改对应列的值。注意,每个列在更新前后占用的存储空间一样大,但凡有一个被更新的列在更新前比更新火占用的存储空间更大,或者更小,都不能进行就地更新。
先删除,再插入
在更新记录时,如果有被更新的列在更新前后占用的存储空间大小不一致,那么就需要先把记录从聚簇索引页中删除,然后再根据更新后列的值插入一条新记录。注意,这里是真正的删除,不是delete mark。
如果新插入的记录占用的存储空间不超过旧记录占用的空间,那么可以直接重用加入到垃圾链表中被删除的旧记录的空间,否则需要重新申请一块空间。
针对UPDATE操作不更新主键的情况,MySQL设计了一种类型为TRX_UNDO_UPD_EXIST_REC的undo日志。
- n_updated:表示本条UPDATE语句执行后将有几个列被更新,<pos, old_len, old_value>表示旧值位置、存储空间大小和真实值。
- 如果UPDATE语句中更新的列包含索引列,那么会添加“索引列各列信息”部分,否则不会添加这个部分。
实际删除:
BEGIN; # 显式开启事务,假设事务id为100
INSERT INTO undo_demo(id, key1, col) VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');
DELETE FROM undo_demo WHERE id = 1;
UPDATE undo_demo SET key1 = 'M249', col = '机枪' WHERE id = 2;
事务执行过程中写入的与UPDATE语句对应的undo日志:
这个UPDATE更新前后,被修改的列没有改变存储空间的大小,属于就地更新。
- 同一个事务中产生的第4条undo日志,所以undo no为3.
- 这条日志的roll_pointer指向undo no为1的那条日志,也就是上次修改这条记录产生的undo日志。
- 更新了索引列key1的值,需要记录“索引列各列信息”部分,也就是主键和key1列的信息。
更新主键时
在聚簇索引中,记录按照主键值的大小练成了一个单向链表。更新主键意味着记录在聚簇索引中的位置将发生改变。针对UPDATE更新主键值的情况,InnoDB存储引擎中分两步进行处理。
- 步骤1:将旧记录delete mark。
- 步骤2:根据更新后各列的值创建一条新记录,并插入到聚簇索引中。
在对记录进行delete mark操作时,会记录一条类型为TRX_UNDO_DEL_MARK_REC的undo日志;之后插入新记录时,会记录一条类型为TRX_UNDO_INSERT_REC的undo日志。也就是说每当一条记录的主键值进行改动,都会产生2条undo日志。注意,在事务提交后,旧记录才被真正的删除。
对于二级索引来说
如果UPDATE语句中没有涉及二级索引的列,就不需要对二级索引进行任何操作。
如果UPDATE语句中涉及到了二级索引的列
- 对旧的二级索引记录执行delete mark
- 根据更新后的值创建一条新的二级索引记录,然后在二级索引对应的B+数中重新定位它的位置并插入进去。
需要注意的是,虽然二级索引记录中不需要记录trx_id、roll_pointer这些属性,但是二级索引记录所在的页面中的Page Header有一个名叫PAGE_MAX_TRX_ID属性。这个属性代表当前页的最大的事务id。
FIL_PAGE_UNDO_LOG类型页面
表空间中的FIL_PAGE_UNDO_LOG类型的页面专门用来存储undo日志,它的通用结构如图所示:
起始偏移量 | 名称 | 大小 |
---|---|---|
0字节 | File Header | 38字节 |
38字节 | Undo Page Header | 18字节 |
56字节 | undo log | 16320字节 |
16376字节 | File Trailer | 8字节 |
File Header和File Trailer与其他类型的页没有什么区别,Undo Page Header的结构如下:
偏移量(字节) | 属性名 | 长度(字节) | 描述 |
---|---|---|---|
38 | TRX_UNDO_PAGE_TYPE | 2 | 表示本页准备存储什么类型的undo日志。 |
40 | TRX_UNDO_PAGE_START | 2 | 表示在本页中第一条undo日志的起始偏移量。 |
42 | TRX_UNDO_PAGE_FREE | 2 | 表示在本页中最后一条undo日志结束时的偏移量。 |
44 | TRX_UNDO_PAGE_NODE | 12 | 表示链表节点结构 |
TRX_UNDO_PAGE_TYPE的可选值只有两个:
- 1:表示TRX_UNDO_INSERT类型。这一类包括TRX_UNDO_INSERT_REC类型的undo日志,由INSERT语句产生,也可以由UPDATE语句更新主键时产生。
- 2:表示TRX_UNDO_UPDATE类型。除了TRX_UNDO_INSERT_REC以外的undo日志都属于这一类,可以由UPDATE、DELETE产生。
注意:在一个UNDO页面中,要么全部存储TRX_UNDO_INSERT大类的undo日志,要么全部存储TRX_UNDO_UPDATE大类的undo日志,不能混合存储。
TRX_UNDO_INSERT_REC类型的undo日志在事务提交后可以直接删除,其他类型的undo日志还不能直接删除。所以将两种类型的undo日志分开存放。
UNDO链表
UNDO页面中TRX_UNDO_PAGE_TYPE属性相同的可以组成一个链表,一共有两种链表,分别为insert undo链表和update undo链表。对普通表和临时表的记录改动所产生的undo日志需要分别记录,不能混用同一个链表。这样一来,一个事务中最多可以有4个UNDO页面组成的链表。
事务在执行过程中按需分配,产生那种类型的undo日志,就分配那种链表;若没有,就不需要分配。
每一个事务之间分配的链表是独立的,各自按需分配。有多少个事务,就要根据每一个事务中对普通表或临时表产生的不同类型的undo日志分配不同的UNDO页面链表。
undo日志的写入过程
段
表空间中的索引的叶子节点和非叶子节点都各自属于一个段(Segment),这样叶子节点和非叶子节点都可以各自分别尽可能存放到一起。每一个段对应一个INODE Entry结构,描述了段的信息,比如段的ID、段内的各种链表基节点、零散页面的页号等。为了定位INODE Entry,undo页面里那标的第一个页面中存放了一个Segment Header结构,该结构如下所示。
属性名 | 大小(字节) | 描述 |
---|---|---|
Space ID of the INODE Entry | 4 | INODE Entry结构所在的表空间ID |
Page Number of the INODE Entry | 4 | INODE Entry结构所在的页号 |
Byte Offset of the INODE Entry | 2 | INODE Entry结构再改页面中的偏移量 |
Undo Log Segment Header
每一个UNDO页面链表都对应一个段,称之为Undo Log Segment。在UNDO链表的第一个页中额外包含了一个名为Undo Log Segment Header的部分,记录了段的Segment Header信息。
每一个Undo链表的第一个页面的结构:
起始偏移量 | 名称 | 大小 |
---|---|---|
0B | File Header | 38B |
38B | Undo Page Header | 18B |
56B | Undo Log Segment Header | 30B |
86B | Undo Log Header | 186B |
272B | undo log | 16064B |
16376B | File Trailer | 8B |
其中Undo Log Segment Header部分的结构如下所示:
起始偏移量 | 名称 | 大小 | 描述 |
---|---|---|---|
56B | TRX_UNDO_STATE | 2B | 表示本UNDO页面链表的状态 |
58B | TRX_UNDO_LAST_LOG | 2B | 表示本UNDO页面链表中最后一个Undo Log Header的位置 |
60B | TRX_UNDO_FSEG_HEADER | 10B | 表示本UNDO页面链表对应的段的Segment Header信息 |
70B | TRX_UNDO_PAGE_LIST | 16B | 表示UNDO页面链表的基节点 |
TRX_UNDO_STATE有5种状态:
- TRX_UNDO_ACTIVE:活跃状态,一个活动旳事务正在向这个UNDO页面链表中写入undo日志。
- TRX_UNDO_CACHED:被缓存的状态,处于该状态的UNDO页面链表等待后续被其他事务重用。
- TRX_UNDO_TO_FREE:等待被释放的状态,insert undo链表对应的事务提交之后,如果该链表不能被重用,就是这种状态。
- TRX_UNDO_TO_PURGE:等待被purge的状态,update undo链表对应的事务提交后,如果链表不能被重用,就处于这种状态。
- TRX_UNDO_PREPARED:处于此状态的UNDO页面链表用于存储处于PREPARE阶段的事务产生的日志。
TRX_UNDO_PAGE_LIST就是每一个页面的Undo Page Header部分中的TRX_UNDO_PAGE_NODE组成的链表的基节点。
Undo Log Header
同一个事务向一个UNDO页面链表中写入的undo日志算是一个组,在每写入一组undo日志时,都会在这组undo日之前先记录一下关于这个组的一些属性,记录在UNDO页面链表中第一个页面Undo Log Header中。
起始偏移量 | 名称 | 大小 | 描述 |
---|---|---|---|
86B | TRX_UNDO_TRX_ID | 8B | 生成本组undo日志的事务id |
94B | TRX_UNDO_TRX_NO | 8B | 事务提交后生成的序号,按提交先后排序 |
102B | TRX_UNDO_DEL_MARKS | 2B | 标记本组undo日志中是否包含由delete mark操作产生的undo日志 |
104B | TRX_UNDO_LOG_START | 2B | 表示本组undo日志中第一条undo日志在页面中的偏移量 |
106B | TRX_UNDO_XID_EXISTS | 1B | 标记本组undo日志是否包含XID信息 |
107B | TRX_UNDO_DICT_TRANS | 1B | 标记本组undo日志是不是由DDL语句产生的 |
108B | TRX_UNDO_TABLE_ID | 8B | 如果TRX_UNDO_DICT_TRANS为TRUE,则本属性表示DDL语句操作的表table id |
116B | TRX_UNDO_NEXT_LOG | 2B | 下一组undo日志在页面中开始的偏移量 |
118B | TRX_UNDO_PREV_LOG | 2B | 上一组undo日志在页面中开始的偏移量 |
120B | TRX_UNDO_HISTORY_NODE | 12B | History链表的节点 |
132B | XID信息 | 140B |
一般情况,一个UNDO页面链表只存储一个事务执行过程中产生的一组undo日志,但是在某些情况下,可能会在一个事务提交后,后续开启的事务又重复利用这个UNDO页面链表,这就导致一个UNDO页面中可能存放多组undo日志。TRX_UNDO_NEXT_LOG和TRX_UNDO_PREV_LOG就用来标记下一组和上一组undo日志在页面中的偏移量。
重用UNDO页面
为什么要重用UNDO页面
当然是为了节约存储空间。
每个事务根据不同的类型,都要各自分配一个UNDO页面链表,但是实际食用量可能会很少,造成浪费。所以要事务提交后在某些情况下进行UNDO页面重用。
重用的原则
想被重用的UNDO链表,必须具备以下条件:
- 链表中只包含一个UNDO页面;
- 这个页面已经使用的空间小于整个页面的3/4。
对于满足了这个条件的UNDO链表,还会根据类型的不同有更细致的规则:
- insert undo链表:事务提交后,在重用这个事务的UNDO链表时,可以直接把之前事务写入的一组undo日志全部覆盖,重新在开头写Undo Log Header以及undo日志。
- update undo链表:事务提交后,UNDO链表中的undo日志不可以立即删除。在重用这个UNDO页面时,只能在同一个页面中写入新的一组undo日志。
update undo链表重用UNDO页面时,就可能重现一个UNDO页面链表中存在多组undo日志。
回滚段
什么是回滚段
Rollback Segment Header页面是一个存放了各个UNDO页面链表的第一个页的页号的页面,这些页号称为undo slot。
回滚段就是每一个Rollback Segment Header页面对应的段。每一个Rollback Segment Header页面都对应一个段,这个回滚段只有这一个页面。
Rollback Segment Header页面结果如下:
起始偏移量 | 名称 | 大小 | 描述 |
---|---|---|---|
0 | File Header | 38B | |
38B | TRX_RSEG_MAX_SIZE | 4B | 设置这个回滚段中管理的所有UNDO页面链表中的UNDO页面数量之和的最大值 |
42B | TRX_RSEG_HISTORY_SIZE | 4B | History链表占用的页面数量 |
46B | TRX_RSEG_HISTORY | 16B | History链表的基节点 |
62B | TRX_RSEG_FSEG_HEADER | 10B | 表这个回滚段对应的Segment Header结构 |
72B | TRX_RSEG_UNDO_SLOTS | 4096B | 各个UNDO页面链表的第一个页面的页号集合,也就是undo slot集合 |
4168B | free space | 15960B | 未使用的空间 |
16376B | File Trailer | 8B |
一个页号占用4字节,对于大小为16KB的页面,TRX_RSEG_UNDO_SLOTS共存储了1024个undo slot,共需4096字节。
多个回滚段
MySQL在系统表空间的第5号页面中存储了128个Rollback Segment Header页面地址(8字节,前4字节为Space ID,后4字节为页号),每个Rollback Segment Header页面对应一个回滚段。每个Rollback Segment Header页面包含1024个undo slot,所以一共能建立131072个UNDO页面链表。
回滚段按照顺序依次从0开始编号,直到第127号回滚段。这些回滚段分为两大类。
- 第一类,第0号、第33~127号。事务对普通表的记录进行修改,必须从这一类回滚段中分配。
- 第二类,第1~32号。事务对临时表的记录进行修改,必须从这一类回滚段中分配。
如果一个事务在执行过程中既对普通表的记录进行修改,又对临时表的记录进行修改,那么需要分别分配2个回滚段。
因为临时表没必要保证持久性。写undo日志,实际也是在UNDO页面中写入记录,写记录就要写redo日志,这样在系统发生崩溃时,可以进行恢复。在修改针对普通表的回滚段中的UNDO页面时,需要记录对应的redo日志;修改针对临时表的回滚段中的UNDO页面时,不需要记录对应的redo日志。
申请UNDO页面链表
初始状态下,一个Rollback Segment Header页面中的TRX_RSEG_UNDO_SLOTS没有存储任何UNDO页面链表第一个页的页号。每一个undo slot的默认值为FIL_NULL,表示该undo slot不指向任何页面。
当一个事务提交时,
- 如果该undo slot指向的UNDO页面链表符合重用条件,该undo slot就处于缓存状态,该UNDO页面链表第一个页的Undo Log Segment Header部分TRX_UNDO_STATE设置为TRX_UNDO_CACHED。
- 如果该链表为insert undo链表,则将该undo slot加入到insert undo cached链表中。
- 如果该链表为update undo链表,则将该undo slot加入到update undo cached链表中。
- 如果该undo slot指向的UNDO页面链表不符合重用条件。
- 如果该链表为insert undo链表,1️⃣该UNDO页面链表的TRX_UNDO_STATE属性被设置为TRX_UNDO_TO_FREE,2️⃣该UNDO页面链表对应的段会被释放,3️⃣该undo slot值被设置为FIL_NULL。
- 如果该链表为update undo链表,1️⃣该UNDO页面链表的TRX_UNDO_STATE属性被设置为TRX_UNDO_TO_PURGE,2️⃣该undo slot的值被设置为FIL_NULL,3️⃣将本次事务写入的一组undo日志放到History链表中(不会释放段,这些undo日志还需要为MVCC服务)。
当有事务申请分配UNDO页面链表时,1️⃣先从对应的cached链表中找,2️⃣如果没有被缓存的undo slot,就去回滚段的Rollback Segment Header中找。遍历TRX_RSEG_UNDO_SLOTS中的undo slot,判断其值是否为FIL_NULL,直到找到值为FIL_NULL的undo slot,在表空间中创建一个Undo Log Segment,然后从这个段中申请一个页面,并把这个页的地址赋值给undo slot。
为事务分配UNDO页面链表详细过程
- 事务对记录进行首次改动前,先去系统表第5号页面分配一个回滚段(回滚段循环使用)。
- 分配回滚段后,先查看回滚段的insert cache和update cache链表有没有缓存的undo slot。如果有,就直接将undo slot分配给事务。
- 如果没有可用的缓存undo slot,就去Rollback Segment Header页面找一个可用的undo slot分配给事务。(1024个全部不能用就直接报错)
- 如果undo slot是从Rollback Segment Header中分配的的,就分配一个Undo Log Segement;如果undo slot从缓存中获得的,就已经存在Undo Log Segement。申请一个UNDO页面作为UNDO链表的first undo page,将页号写入undo slot中。
- 在申请的UNDO页面中写入undo日志。
undo日志在崩溃恢复时的作用
服务器的恢复步骤:
- 根据redo日志将各个页面恢复到崩溃前的状态,保证已提交的事务的持久性。
- 将未提交的事务全部回滚,保证事务的原子性。
- 从表空间第5号页定位到128个回滚段,遍历每一个回滚段的每一个undo slot找到值不为FIL_NULL的undo slot。
- 根据undo slot的页号找到对应的UNDO页面链表第一个页,根据该页的Undo Segment Header部分的TRX_UNDO_STATE属性的值进行判断。如果该值为TRX_UNDO_ACTIVE,就根据Undo Segment Header中的TRX_UNDO_LAST_LOG属性找到UNDO链表中最后一个Undo Log Header的位置。
- 从该Undo Log Header中找到对应事务的事务id,通过undo日志记录的信息,对事务所产生的影响进行回滚。
TRX_UNDO_STATE为TRX_UNDO_ACTIVE表示当前有正在执行的事务在向这个UNDO页面链表中的页面写入undo日志。找到这个UNDO链表的最后一个undo日志组,因为前面的一定已经提交了。
阅读学习《MySQL是怎样运行的》