文章目录
- 一、前言
- 二、undo 日志(回滚日志)
- 1. 事务 id
- 2. undo 日志格式
- 2.1 INSERT 对应的 undo 日志
- 2.2 DELETE 对应的 undo 日志
- 2.3 UPDATE 对应的 undo 日志
- 2.3.1 不更新主键
- 2.3.2 更新主键
- 2.3 增删改操作对二级索引的影响
- 2.4 roll_pointer
- 3. FIL_PAGE_UNDO_LOG 页面
- 4. Undo 页面链表
- 5. Undo 日志具体写入过程
- 6. 重用 Undo 页面
- 7. 回滚段
- 7.1 概念
- 7.2 从回滚段申请 Undo 页面链表
- 7.3 多个回滚段
- 7.4 回滚段的分类
- 7.5 roll_pointer 的组成
- 7.6 事务分配 Undo 页面链表的过程
- 7.7 undo 日志在崩溃恢复时的作用
- 四、总结
- 五、参考内容
一、前言
最近在读《MySQL 是怎样运行的》、《MySQL技术内幕 InnoDB存储引擎 》,后续会随机将书中部分内容记录下来作为学习笔记,部分内容经过个人删改,因此可能存在错误,如想详细了解相关内容强烈推荐阅读相关书籍。
二、undo 日志(回滚日志)
redo 日志记录了事务的行为,可以很好的通过其对页进行 “重做”。但是事务有时候还需要回滚操作,这是就需要undo 日志,undo 日志保证了事务的原子性。
在对 DB 进行修改时, InnoDB不仅会产生 redo 还会产生一定量的 undo, 如果事务需要进行回滚,则可以利用这些undo 信息回滚。与 redo 存放在重做日志文件中不同的是, undo 存放在数据库内部的一个特殊段(segment)中,称为 undo 段,undo 段位于共享表空间中。
undo 日志并非是将数据库物理地恢复到执行语句或事务之前的样子:undo 日志是逻辑日志,因此只是将数据库逻辑地恢复到原来的样子,所有的修改都被逻辑地取消了,但是数据结构和页本身在回滚之后可能并不相同。因为在系统中可能存在多个事务并发操作,数据库的任务就是协调数据记录的并发访问。比如一个事务修改当前页中某几条记录,同时还有别的事务在对当前页进行修改,所以不能将一个页回滚到事务开始的样子,不然会影响其他事务工作。
1. 事务 id
一个事务可以是只读事务,也可以是读写事务,如果一个事务在执行过程中对某个表执行了增删改操作,那么 InnoDB 会为其分配一个事务id。
事务id 就是一个递增的数字, 其分配策略如下:
- 服务器会在内存中维护一个全局变量,每当需要为某个事务分配事务id时,就会把该变量的值当做事务id分配给该事物,并且把该变量自增1
- 当这个变量的值是 256 的倍数时,就会将该变量的值刷新到系统表空间页号为5 的页面中一个名为 Max Trx ID 的属性中,该属性占用 8 字节的存储空间
- 当系统下一次启动时会将 Max Trx ID 属性加载到内存中,将该值加上 256 之后赋值给前面提到的全局变量。
事务id对事务的分配方式如下:
- 对于只读事务,只有在他第一次对某个用户创建的临时表增删改操作时会为这个事务分配一个事务id,否则不分配事务id,默认为0。
- 对于读写事务来说,只有在他第一次对某个表(包括用户创建的临时表)执行增删改操作时,才会为这个事务分配一个事务id,否则不分配事务id,默认为0。
在 【MySQL02】【 InnoDB 记录存储结构】提到过 InnoDB 记录行格式中聚簇索引会存在 row_id、trx_id、roll_pointer 三个隐藏列,而 trx_id 中保存的就是对当前记录进行修改的所在事务的事务id。如下图:
2. undo 日志格式
一个事务执行过程中可能会对应多条语句,就会需要记录多条对应的 undo 日志。这些日志会从 0 开始编号,根据生成顺序可以称为 第 0号 undo 日志,第 1 号 undo 日志等,这个编号也称为 undo no。
undo 日志会被记录到类型为 FIL_PAGE_UNDO_LOG 的页面中,这些页面可以从系统表空间分配,也可以从 undo 表空间(专门存放 undo 日志的表空间)分配。
2.1 INSERT 对应的 undo 日志
在进行 INSERT 操作时, undo 日志只需要记录新插入的记录的主键值即可,如果需要回滚则通过主键值将插入记录删除即可。
INSERT 对应的 undo 日志格式为 TRX_UNDO_INSERT_REC,结构如下:
其中各个字段的解释如下:
字段 | 解释 |
---|---|
end of record | 本条 undo 日志结束,下一条开始时在页面中的地址 |
undo type | 本条undo 日志的类型,即 TRX_UNDO_INSERT_REC |
undo no | 本条undo日志对应的编号 |
table id | 本条 undo 日志对应的记录所在表的table_id |
主键各列信息 | 主键字段每个列占用的存储空间大小和真实值 |
start of record | 上一条 undo 日志结束,本条开始时在页面中的地址 |
这里注意两点:
- undo 在一个事务是从 0 开始递增的,只要事务没提交,每生成一条 undo 日志,那么该条日志的 undo 就增1。
- 如果记录中的主键只包含一列,那么在 TRX_UNDO_INSERT_REC 类型的日志中,只需要把该列占用的存储空间大小和真实值记录下来。如果记录中的主键包含多个列(联合主键),那么每个列占用的存储空间大小和对应的真实值都需要记录下来。(len 代表列占用的存储空间大小;value 代表列的真实值)
当进行插入操作时,实际上需要向聚餐索引和所有二级索引都插入一条记录,不过在记录 undo 日志时,只需要针对聚簇索引记录来记录一条 undo 日志就好,聚簇索引记录和二级索引记录是一一对应的,在回滚 INSERT 操作时,只需要根据这条记录的主键信息进行删除操作,在删除时会将聚簇索引和所有对应的二级索引都删除。
2.2 DELETE 对应的 undo 日志
在【MySQL02】【 InnoDB 记录存储结构】中提到过,插入到页面的记录会根据记录头信息中的 next_record 属性组成一个单向链表,暂且成为正常记录链表。而被删除的记录也会通过next_record 属性组成一个链表,不过这个链表中的记录占用的存储空间可以被重新利用,所以也称这个链表为垃圾链表。而每个数据页的 Page Header 部分中有一个名为 PAGE_FREE 的属性指向被删除记录组成的垃圾链表中的头节点。数据页结构如下图:
记录的删除需要经历两个阶段:
- delete mark 阶段 : 将记录的 deleted_flag 标志位置为 1,同时修改记录的 trx_id、roll_pointer 的值。在删除语句所在的事务提交之前,被删除的记录一直都处于这种中间状态。
- purge 阶段:当删除该语句所在的事务提交之后,会有专门的线程来将记录真正的删除掉,这里所谓真正的删除就是将该记录从正常链表中移除,并且加到垃圾链表中(这里是加入到链表的头节点,并修改 PAGE_FREE 指向新删除的记录),除此之外还会修改一些页面属性。
在 purge 阶段执行结束后,记录就可以确定被删除了,空间也可以重复利用了。
如何重复利用垃圾链表的空间?
Page Header 存在一个名为 PAGE GARBAGE 的属性,记录这当前页面中可重用存储空间占用的总字节数,每当有已删除记录加入到垃圾链表后,都会将 PAGE GARBAGE 属性的值加上已删除记录占用的存储空间的大小。PAGE_FREE 指向垃圾链表的头节点,之后每当插入新的记录时,会首先判断垃圾链表中的头节点代表的已删除的记录所占用的空间是否足够容纳这条新插入的记录。如果无法容纳就直接向页申请新的空间来存储这条记录(并不会遍历所有的垃圾链表,而是直接用头节点判断)。如果可以容纳就直接重用这条已删除记录的存储空间,并让 PAGE_FREE 指向垃圾链表的下一条记录。碎片空间的重组
这就会存在一个问题:如果新插入的记录无法完全占用已删除记录的存储空间,就意味着头节点对应的记录所占用的存储空间中有一部分空间未使用到,形成了碎片空间。而这些未被使用的碎片空间占用的存储空间的大小会被统计到 PAGE_GARBAGE 属性中,当页空间快满时如果在插入一条新纪录,此时页并不能分配足够的空间时就会判断PAGE_GARBAGE 的空间和剩余可利用的空间相加之后是否可以容纳这条记录,如果可以 InnoDB 会尝试重新组织页内的记录。重新组织的过程是先开辟出一个临时页将页面内的记录依次插入一遍,然后再把临时页面的内容复制到本页面,这样就可以把那些碎片空间都解放出来。
以下图为例:其中 记录1,2,3 是未删除数据,因此在正常数据链表中,而 记录 4,5 是已经打上删除标记的"已删除"记录,在垃圾链表中,上面提到数据页的 Page Header 部分中的 PAGE_FREE 属性指向垃圾链表的节点的头指针。
我们假设某个事务需要删除记录3 会经历如下阶段:
- delete mark 阶段 :此阶段会将记录3 的 delete_flag 置为 1 但是并不会将记录3 移动到 垃圾链表中。此时记录3既不是正常记录,也不是已删除记录,而是处于一个中间状态,如下图:
- purge 阶段 :该阶段将 记录3 从正常记录链表中移除并添加到垃圾链表中,需要注意这里是将记录3 添加到垃圾链表表头。
综上可以得知一个事务在删除一条记录时,只需要经历 delete mark 阶段,而一旦事务提交就不需要再回滚这个事务了。
2.3 UPDATE 对应的 undo 日志
InnoDB 对 UPDATE 操作分为更新逐渐和不更新两种情况,不同的情况有不同的处理方式。
2.3.1 不更新主键
不更新主键还可以划分为更新的列占用的存储空间发生变化和不发生变化两种情况。
-
就地更新(in-place update): 在更新的时候,对于每个被更新的列来说,如果更新的列更新前后所占用的存储空间一样大,则可以进行就地更新,也就是在原纪录的基础上修改对应列的值。
-
先删除旧记录再插入新记录 :在不满足就地更新的条件时,就需要先删除旧记录后再更具更新后的列的值创建一条新的记录并插入到页面中。需要注意这里所说的删除不是 delete mark 操作,而是真正删除,即把这条记录从正常记录链表移除并加入到垃圾链表中,并修改页面中相应的统计信息(如 PAGE_FREE、PAGE_GARBAGE 等)。并且这里执行真正删除操作的线程是用户线程,而并非专门进行 purge 操作的线程。在真正删除之后就会根据各个列更新后的值创建一条新的记录并将这条记录插入页面中。
如果新创建的记录占用的存储空间不超过旧记录占用的空间,就可以直接重用加入到垃圾链表中的旧记录所占用的存储空间,否则就要在页面中新申请一块空间供新纪录使用。如果本页面内已经没有可用的空间,就需要进行页分裂操作,然后再插入新记录。
2.3.2 更新主键
在聚簇索引中,记录按照主键大小值排序成一个单向链表,如果更新了某条记录的主键值就意味着这条记录在聚簇索引中的位置即将发生改变。(如果主键从1 变更为 100000,由于中间数据量很多,物理层面可能跨了好几个页面),这种情况 InnoDB 在聚簇索引中分了两步进行处理。
- 将旧记录进行 delete mark 操作。
- 根据更新后的各列的值创建一条新记录,并将其插入到聚簇索引中:由于更新后的记录主键值发生了改变,所以需要重新从聚簇索引中定位这条记录所在的位置,然后将其插进去。
针对这种情况,会产生两条 undo 日志,在进行 delete mark 进行操作时,记录一条 undo 日志;之后重新插入时还会一条 undo 日志,也就是说每对一条记录的主键值进行改动都会记录 2条 undo 日志。
2.3 增删改操作对二级索引的影响
上面说的都是增删改对聚簇索引记录所做的影响。对于二级索引记录来说, INSERT 和 DELETE 操作与在聚簇索引中执行时产生的影响差不多,但 UPDATE 稍有不同。
当更新语句更新的列并不涉及二级索引,则无需对二级索引做任何操作;如果更新语句的更新列涉及二级索引,则会进行下面两个操作:
- 对旧的二级索引记录执行 delete mark 操作
- 根据更新后的值创建一条新的二级索引记录,然后在二级索引对应的 B+Tree 中重新定位到他的位置并插进去。
每修改一条二级索引也会影响这条二级索引所在的页面的 Page Header 部分中一个名为 PAGE_MAX_TRX_ID 的属性,这个属性代表修改当前页的最大的事务id。
2.4 roll_pointer
roll_point 本质上就是指向记录对应的undo 日志的指针,如下图:当向表中依次插入了记录1 和 记录2 两条记录,此时记录行的 roll_pointer 就会指向对应 undo 页的 undo 日志。
3. FIL_PAGE_UNDO_LOG 页面
表空间实际是由许多页面构成的,页面默认为16KB,这些页面有不同的类型,其中有一种名为 FIL_PAGE_UNDO_LOG 类型的页面是专门用来存储 undo 日志的。这种页面的通用格式如下:
图中 File Header 和 File Trailer 是各种页面的通用结构,在【MySQL02】【 InnoDB 记录存储结构】有过详细介绍,这里不再赘述。Undo Page Header 部分是 FIL_PAGE_UNDO_LOG 页面(下简称 Undo 页面)独有的,结构如下:
其中各个属性的解释如下:
属性 | 解释 |
---|---|
TRX_UNDO_PAGE_TYPE | 当前页面准备存储的 undo 日志类型。简单来说可以分为 TRX_UNDO_INSERT(一般由 INSERT 语句产生,当 UPDATE 语句中有更新主键的情况时也会产生该类型的日志,称为 insert undo 日志)和 TRX_UNDO_UPDATE (除了 TRX_UNDO_INSERT 类型之外的其他类型都属于该类型,称为 update undo 日志) |
TRX_UNDO_PAGE_START | 表示当前页面从什么位置开始存储 undo 日志,或者说第一条 undo 日志在本页面中的起始偏移量 |
TRX_UNDO_PAGE_FREE | 表示当前页面中存储的最后一条 undo 日志结束时的偏移量,或者说从这个位置开始,可以往后继续写入新的 undo 日志。当一条undo 日志没写时TRX_UNDO_PAGE_FREE 等于 TRX_UNDO_PAGE_START |
TRX_UNDO_PAGE_NODE | 代表一个链表节点结构,下面会将。 |
4. Undo 页面链表
一个事务中可能包含多个语句,一个语句可能修改多条记录,一条记录的修改可能会产生多条 undo 日志记录,因此一个事务执行过程中可能产生很多 undo 日志,这些日志可能一个页面中放不下,需要放到多个页面中,这些页面就通过上面介绍的 TRX_UNDO_PAGE_NODE属性连成了链表,如下图:
first undo page 中除了包含 Undo Page Header 之外,还会包含其他一些管理信息,结构如下:
在一个事务执行过程中,可能会混合着增删改的操作,也就会产生多种类型的 undo 日志,但是同一个Undo 页面要么只存储 TRX_UNDO_INSERT大类的 undo 日志,要么只存储 TRX_UNDO_UPDATE 大类的 undo 日志,不能混着存储,所以在一个事务执行过程中就可能需要两个 Undo 页面链表,一个称为 insert undo 链表,一个称为 update undo 链表,如下:
同时InnoDB 规定,对普通表和临时表的记录改动时产生的 undo 日志要分别记录,所以在一个事务中最多有4个 以 undo 页面为节点组成的链表(这四个链表并不是在事务开始时就会分配的,而是需要使用的时候才会分配。)。如下图:
注意:
- 为了尽可能提高 undo 日志的写入效率,不同事务执行过程中产生的 undo 日志需要写入不同的 Undo 页面链表。如果有更多的事务就会产生更多的 Undo 页面链表。
- InnoDB 规定,每个 Undo 页面链表都对应着一个段,称为 Undo Log Segment。也即是说链表中的页面都是从这个段中申请的,所以在 Undo 链表的第一个页面(first undo page)中设计了一个 Undo Log Segment Header 部分,这个部分主要包含了该链表对应的段的 Segment Header 信息,以及其他一些关于这个段的信息。
5. Undo 日志具体写入过程
一个事务在向 Undo 页面中写入 undo 日志时,是一条接一条相邻写入的。写完一个 Undo 页面后,再从段中申请一个新页面,然后把这个页面插入到 Undo 页面链表中,继续往这个新的页面中写 undo 日志。
对于没有被重用的 Undo 页面链表来说,链表的第一个页面(first undo page)在真正写入 undo 日志前,会填充 Undo Page Header、Undo Log Segment Header、Undo Log Header 三部分,之后才会正式写入日志。对于其他页面(normal undo page)来说,在真正写入undo 日志前,只会填充 Undo Page Heade。链表基节点存放到 first undo page 的 Undo Log Segment Header 部分,链表节点信息存放到每个 Undo 页面的 Undo Page Header 部分,如下图:
6. 重用 Undo 页面
上文提到 InnoDB 规定会为每个事务单独分配对应的 Undo 页面链表,即一个 Undo 页面链表只存储一个事务执行过程中产生的一组 undo 日志,这就可能造成空间浪费。如大部分事务在执行过程中可能只修改了一条或几条记录,针对某个 Undo 页面链表只产生了少了 undo 日志,这些少量 的 undo 日志只会占用很少的存储空间,但是因为每开启一个事物就新创建一个 Undo 页面链表(即使这个链表中只有一个页面)来存储这些少量的 undo 日志,因此会造成链表中的页面的空间浪费。
因此 InnoDB规定 在事务提交后的某些情况下可以重用该事务的 Undo 页面链表。重用 Undo 页面需要满足如下条件:
- 该链表中只包含一个 Undo 页面
- 该 Undo 页面已经使用的空间小于整个页面空间的 3/4
Undo 页面链表被分为 insert undo 链表和 update undo 链表两种,这两种链表在重用时策略也是不同的,如下:
- insert undo 链表:这种类型的undo日志在事务提交后就没用了,可以被清除掉,因此在某个事务提交后,在重用这个事务的 insert undo 链表时,可以直接把之前事务写入的一组 undo 日志覆盖掉,从头开始写入新事物的一组 undo 日志。
- update undo 链表:在一个事务提交后,它的 update undo 链表中的 undo 日志不能立即删除(需要用于 MVCC)。这样之后的事务想重用 update undo 链表时就不能覆盖之前事务写入的 undo 日志,这样就相当于在同一个 Undo 页面写入了多组 undo 日志。
7. 回滚段
7.1 概念
一个事务在执行过程中最多可以分配 4 个 Undo 页面链,而在同一时刻的不同事务拥有的 Undo 页面链表是不同的,系统在同一时刻可以存在多个 Undo 页面链表。为了更好的管理这些链表,InnoDB 提出了名为 Rollback Segment Header 的页面,这个页面中可以存放各个 Undo 页面链表的 first undo page 的页号,这些页号称为 undo slot。
InnoDB 规定,每个 Rollback Segment Header 页面都对应着一个段,这个段称为回滚段(Rollbakc Segment),与其他段不同,回滚段中其实只有一个页面。
Rollback Segment Header 的结构如下:
其中各个字段的含义如下:
字段名 | 解释 |
---|---|
TRX_RESG_MAX_SIZE | 这个回滚段中管理的所有 Undo 页面链表中的 Undo 页面数量之和的最大值。即在这个回滚段中,所有 Undo 页面链表中的 Undo 页面数量之和是不能超过该值,该属性默认无限制。 |
TRX_RESG_HISTORY_SIZE | History 链表所占用的页面数量 |
TRX_RESG_HISTORY | History 链表的基节点 |
TRX_RESG_FSEG_HEADER | 这个回滚段对应的 10字节大小的 Segment Header 结构,通过他可以找当前回滚段对应的 INODE Entry |
TRX_RESG_UNDO_SLOTS | 各个 Undo 页面链表的 first undo page 的页号集合,也就是undo slot 集合 |
7.2 从回滚段申请 Undo 页面链表
在初始情况下,由于未向任何事物分配任何 Undo 页面链表,所以对于一个 Rollback Segment Header 页面来说,他的各个 undo slot 都被设置为一个特殊的值:FIL_NULL,这表示该 undo slot 不指向任何页面。
随着时间的流逝,开始有事务需要分配 Undo 页面链表了,于是从回滚段的第一个 undo slot 开始寻找:
- 如果 undo slot 的值是 FIL_NULL,那么就在表空间中新创建一个段(Undo Log Segment),然后从段中申请一个页面作为 Undo 页面链表的 first undo page,最后把该 undo slot 的值设置为刚刚申请的这个页面的地址,这也就意味着这个 undo slot 被分配给了这个事务。
- 如果不是为 FIL_NULL,则说明该 undo slot 已经指向了一个 undo 链表,也就是说这个 undo slot 已经被其他事务占用了,这就需要跳到下一个 undo slot 重复上述判断逻辑。
如果一个 Rollback Segment Header 页面中包含的 1024 个 undo slot 的值都不为 FIL_NULL,则新事物无法再获得新的 Undo 页面链表,就会停止执行事务并向用户报错:
Too many active concurrent transactions
当一个事务提交时,其占用的 undo slot 有两种可能:
-
如果该 undo slot 符合被重用的条件,则该 undo slot 就处于被缓存的状态。InnoDB规定,该 Undo 页面链表的 TRX_UNDO_STATE 属性会被设置为 TRX_UNDO_CACHED。
被缓存的 undo slot 都会被加入到一个链表中,不同的 Undo 页面链表(insert undo 链表和 update undo 链表)对应的 undo slot 会被加入到不同的链表中。
一个回滚段对应着上述两个 cached 链表,如果有新事物要分配 undo slot 都先从对应的 cached 链表中招,如果没有被缓存的 undo slot,才会到回滚段的 Rollback Segment Header 页面中寻找。 -
如果该 undo slot 不符合被重用的条件,那么根据该 undo slot 对应的 Undo 页面链表类型的不同也会有不同的处理。
- 如果是 insert undo 链表,则该 Undo 页面链表的 TRX_UNDO_STATE 属性会被设置为 TRX_UNDO_TO_FREE 。之后该 Undo 页面链表对应的段会被释放掉,然后把该 undo slot 的值设置为 FIL_NULL。
- 如果是 update undo 链表,则该 Undo 页面链表的 TRX_UNDO_STATE 属性会被设置为 TRX_UNDO_TO_PURGE,并将该 undo slot 的值设置为 FIL_NULL,然后将本次事务写入的一组 undo 日志放到 History 链表中(这里并不会将 Undo页面链表对应的段释放掉,因为MVCC 还需要使用)
7.3 多个回滚段
由于一个回滚段最多只支持1024个事务并发,这显然是不够的,所以 InnoDB 设置了128个回滚段,如下:
7.4 回滚段的分类
对128个回滚段从0开始编号,即最开始的回滚段称为 0 号回滚段,最后的回滚段称为 127 号回滚段,这128个回滚段可以分为两大类:
-
第 0号,第33~127号回滚段属于一类:其中第 0 号回滚段必须在系统表空间中,第 33~127号回滚段既可以在系统表空间也可以在自己配置的 undo 表空间中。
如果一个事务在执行过程中对普通表的记录进行了改动,需要分配 Undo 页面链表,则必须从这一类段中分配相应的 undo slot。
-
第1~32号回滚段属于一类,这些回滚段必须在临时表空间(对应数据目录中的 ibtmp1 文件)中。
如果一个事务在执行过程中对临时表的记录进行了改动,需要分配 Undo 页面链表,则必须从这一类段中分配相应的 undo slot。
如果一个事务在执行过程中既对普通表记录进行改动,又对临时表的记录进行改动,则就需要为这个记录分配两个回滚段。
之所以分为两种回滚段,是因为临时表的修改产生的undo 日志只需要在系统运行过程中有效即可,如果系统崩溃,则重启后也不需要恢复这些 undo 日志所在的页面,所以在针对临时表写的 undo 页面时并不需要记录相应的 redo 日志,而对于普通表的修改不仅要记录其 undo 日志,还要记录 redo 日志。
回滚段的数量和存储位置都可以通过配置来指定,本文不再赘述。
7.5 roll_pointer 的组成
roll_pointer 的结构如下:
各个属性的意义如下:
属性 | 解释 |
---|---|
is_insert | 表示该指针指向的 undo 日志是否是 TRX_UNDO_INSERT 大类的 undo 日志 |
rseg_id | 表示该指针指向的 undo 日志的回滚段编号,即0~127 回滚段编号 |
page number | 表示该指针指向的 undo 日志所在的页面的页号 |
offset | 表示该指针指向的 undo 日志在页面中的偏移量 |
roll_pointer 基于上述内容便可以定位到一条 undo 日志。
7.6 事务分配 Undo 页面链表的过程
-
事务在执行过程中对普通表的记录进行首次改动之前,会首先到系统表空间的第五号页面分配(轮询方式分配)一个回滚段(就是获取一个 Rollback Segment Header 页面的地址)。一旦某个回滚段被分配给了这个事务,之后该事务再对普通表的记录进行改动时就不会重新分配了。
-
在分配到回滚段后,首先判断回滚段的两个 cache 链表有没有已经缓存的 undo slot。如果有缓存的 undo slot 就将缓存的 undo slot 分配给当前事务。
-
如果没有缓存的 undo slot ,则需要从 Rollback Segment Header 中找到一个可用的 undo slot 分配给该事物。分配的逻辑上面已经提到:
从 Rollback Segment Header 的第0 个 undo slot 开始判断:
- 如果 undo slot 的值是 FIL_NULL,那么就在表空间中新创建一个段(Undo Log Segment),然后从段中申请一个页面作为 Undo 页面链表的 first undo page,最后把该 undo slot 的值设置为刚刚申请的这个页面的地址,这也就意味着这个 undo slot 被分配给了这个事务。
- 如果不是为 FIL_NULL,则说明该 undo slot 已经指向了一个 undo 链表,也就是说这个 undo slot 已经被其他事务占用了,这就需要跳到下一个 undo slot 重复上述判断逻辑。
-
找到可用的 undo slot 后,如果该 undo slot 是从 cached 链表获取的,那么其对应的 Undo Log Segment 就已经分配了,否则需要重新分配一个 Undo Log Segment,然后从该 Undo Log Segment 中申请一个页面作为 Undo 页面链表的 first undo page,并把该页的页号填入获取的 undo slot 中。
-
然后事务就可以把 undo 日志写入到上面申请的Undo 页面链表了。
7.7 undo 日志在崩溃恢复时的作用
当服务器崩溃后恢复时,首先需要按照 redo 日志将各个页面的数据恢复到崩溃之前的状态,这样可以保证已提交事务的持久性,但这样有一个问题即:未提交事务写的 redo 日志可能也已经刷盘,那么这些未提交的事务在修改过的页面在 MySQL 重启时可能也被恢复了。因此为了保证事务的原子性,需要将未提交事务的 redo 日志回滚掉。
在 【MySQL04】【 redo 日志】 中有过介绍为什么不在事务提交时将脏页刷新,主要因为下面的问题
- 刷新一个完整的页面太浪费了,有时候仅仅修改了页面的一个字节也需要将整个页刷新到磁盘。
- 随机IO刷新效率太低。一个事务可能包含很多语句,即使一个语句也可能修改很多页面,并且这些页面可能并不相邻,在将这些页面刷新到磁盘的时候需进行很对随机IO。
因为上述问题,InnoDB 并不是在事务提交时将所有页刷新到磁盘,而是将修改的内容记录下来,在合适的时机刷新到磁盘中。
InnoDB 通过系统表空间第5号页面定位到 128 个回滚段的位置,在每个回滚段的 1024 个 undo slot 中找到那些值不为 FIL_NULL 的 undo slot ,每一个 undo slot 对应一个 Undo 页面链表。然后从 Undo 页面链表第一个页面的 Undo Segment Header 中找到 TRX_UNDO_STATE 属性,该属性标识当前 Undo 页面链表所处的状态,如果该属性的值为 TRX_UNDO_ACTIVE ,则意味着有一个活跃的事务在向这个 Undo 页面链表中写入 undo 日志。然后再在 Undo Segment Header 中找到 TRX_UNDO_LAST_LOG 属性,通过该属性可以找到本 Undo 页面链表最后一个 Undo Log Header 的位置,从该 Undo Log Header 中可以找到对应事务的事务id以及一些其他信息,则该事物id对应的事务就是未提交的事务,通过 undo 日志中记录的信息将该事物对页面所做的更改全部回滚掉,这样就保证了事务的原子性。
四、总结
一个事务最多有四个 Undo 页面链表, 一个 Undo 页面链表至少有一个 Undo页面,一般来说一个 Undo 页面链表在不重用的情况下只能被一个事务使用。
InnoDB中存在多个回滚段,一个回滚段默认支持 1024个事务并发,因此最多存在 1024个 Rollback Segment Header ,而 每个 Rollback Segment Header 都存在多个 undo slot,每个 undo slot 中保存的是 undo 页面链表的 first undo page,根据 first undo page 就可以找到完整的 undo 页面链表。
五、参考内容
书籍:《MySQL是怎样运行的——从根儿上理解MySQL》、《MySQL技术内幕 InnoDB存储引擎 》
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正