InnoDB中 redo log 和 undo log

1. InnoDB中的Buffer Pool

Buffer Pool 的大小由系统变量 innodb_buffer_pool_size 控制,最小为 5MB 。

innodb_buffer_pool_size 的值小于 1GB 时,innodb_buffer_pool_instances强制设置为 1。

1.1 控制块

(1)每一个缓冲页都有一些控制信息,包含该页所属的表空间 ID、页号、链表节点信息等。每个缓冲页对应的控制信息占用内存大小是相同的,称为控制块

(2)控制块与缓冲页一一对应,存放在 Buffer Pool 前面,innodb_buffer_pool_size 并不包含控制块占用的内存空间大小。

1.2 链表

(1)链表的基节点包含头尾指针、当前链表节点数量,注意该基节点本身存储在另一块单独申请的内存空间中。

free 链表中实际内容为各控制块。

flush 链表存储的是被修改过的缓冲页。

LRU 链表优化:只有被访问的缓冲页位于 young 区域的后 1/4 页面时,才会被移动到 LRU 链表的头部。

(2)刷新脏页到磁盘:

  1. 后台进程定时从 LRU 链表尾部开始扫描一些页面,发现脏页后就刷新到磁盘中。
  2. 后台进程从 flush 链表中刷新一部分到磁盘
  3. 用户进程从 LRU 链表尾部刷新一个脏页到磁盘。(后台进程刷新较慢,用户加载一个页面时没有空间)

(3)以 chunk 为单位向操作系统申请内存空间。innodb_buffer_pool_chunk_size 只能在服务器启动时指定。

2. redo 日志

通用结构为:type | space ID | page number | data

2.1.1 简单的 redo 日志类型
类型 type字段对应数字(十进制) 描述
MLOG_1BYTE 1 表示在页面的某个偏移量处写入 1 个字节数据
MLOG_2BYTE 2 表示在页面的某个偏移量处写入 2 个字节数据
MLOG_4BYTE 4 表示在页面的某个偏移量处写入 4 个字节数据
MLOG_8BYTE 8 表示在页面的某个偏移量处写入 8 个字节数据
MLOG_WRITE_BYTE 30 表示在页面的某个偏移量处写入一个字节序列

type | space ID | page number | offset | data

其中 MLOG_WRITE_BYTE 的结构为:

type | space ID | page number | offset | len(具体占用多少字节) | data

(1)例子

服务器在内存中维护一个全局变量,每当向某个包含 row_id 隐藏列的表中插入一条数据时,就会将当前全局变量的值做为新纪录的 row_id 列的值,并将该全局变量自增 1;
每当这个全局变量的值为 256 的倍数时,就会写入到系统表空间页号为 7 的页面中一个名为 MAX ROW ID 的属性中(以 redo 日志方式写入);
当系统启动时,会将 MAX ROW ID 加载到内存中,并将该值加上 256 之后赋值给全局变量;

2.1.2 复杂一点的 redo 日志类型

在语句执行过程中,INSERT 对所有页面的修改都需要保存到 redo 日志中。

除了实际记录外,还可能更改 Page Header、Page Directory 等部分,甚至发生页的分裂。

类型 type字段对应数字(十进制) 描述
MLOG_REC_INSERT 9 插入一条使用非紧凑行格式( REDUNDANT ) 的记录
MLOG_COMP_REC_INSERT 38 插入一条使用紧凑行格式( COMPACT、DYNAMIC、COMPRESSED) 的记录
MLOG_COMP_PAGE_CREATE 58 创建一个存储紧凑行格式记录的页面
MLOG_COMP_REC_DELETE 42 删除一个存储紧凑行格式记录的页面
MLOG_COMP_LIST_START_DELETE 44 从某条给定记录开始删除页面中一系列使用紧凑行格式的记录
MLOG_COMP_LIST_END_DELETE 43 与 MLOG_COMP_LIST_START_DELETE 对应,表示删除一系列记录的末尾
MLOG_ZIP_PAGE_COMPRESS 51 压缩一个数据页

这些记录只是记录了必要的信息并不完全,例如没有记录怎么修改 Page Directory 等,在进行恢复时,实际上是重播,即调用函数重新进行插入操作。

2.2 Mini-Transaction

Mini-Transaction(MTR):对底层页面进行一次原子访问的过程。

2.2.1 以组的形式写入 redo 日志

(1)将日志分为若干组,这些组都是不可分割的(具有原子性,要么全恢复,要么一条也不恢复)

更新 Max Row ID 属性时产生的 redo 日志为一组
向聚簇索引对应的 B+ 树的页面中插入一条记录时产生的 redo 日志为一组
向二级索引对应的 B+ 树的页面中插入一条记录时产生的 redo 日志为一组

(2)有些需要保证原子性的操作生成多条 redo 日志,在该组最后一条 redo 日志后面加上一个特殊类型的 redo 日志(MLOG_MULTI_REC_END,该类型只有一个 type 字段(十进制的 31));

有些需要保证原子性的操作只生成一个 redo 日志(在该日志的 type 字段的最高位标识产生单个/一系列的 redo 日志)

2.3 日志写入过程

InnoDB中 redo log 和 undo log_第1张图片
LOG_BLOCK_HDR_NO:每个 block 的编号;
LOG_BLOCK_HDR_DATA_LEN:表示该 block 中已写入的字节数,初始为 12;
LOG_BLOCK_FIRST_REC_GROUP:该 block 中第一个 MTR 生成的 redo log 偏移量;
LOG_BLOCK_CHECKPOINT_NO:表示 checkpoint 的序号;

LOG_BLOCK_CHECKSUM:校验和;

2.3.1 log buffer

在内存中有个 buf_free 的全局变量指向后续写入的 redo 日志应该写到 log buffer 中的哪个 block 的哪个位置。

并不是每生成一条 redo 日志就将其插入到 log buffer 中,而是将每个 MTR 运行过程中产生的日志先暂存到某个地方,当 MTR 运行结束后,再将该组 redo 日志全部复制到 log buffer 中。

不同事务可能是并发执行的,因此不同事务的 MTR 可能是交替写入 log buffer 中。

2.3.2 redo log 刷盘时机

log buffer 空间不足时
事务提交时
后台进程每个 1s 将 log buffer 中的 redo 日志刷新到磁盘
正常关闭服务器时
做 checkpoint 时

2.3.3 redo log 文件

redo 日志文件也是由若干个 512 字节的 block 组成的。
前 4 个 block 存放一些管理信息。
InnoDB中 redo log 和 undo log_第2张图片
LOG_HEADER_START_LSN:标记本 redo 文件偏移量为 2048 字节处对应的 lsn 值。

checkpoint1 和 2 的结构如下:
InnoDB中 redo log 和 undo log_第3张图片
LOG_CHECKPOINT_NO:服务器执行 checkpoint 的编号;
LOG_CHECKPOINT_LSN:服务器在结束 checkpoint 时对应的 lsn 值,崩溃后恢复时从该值开始;
LOG_CHECKPOINT_OFFSET:上个属性中的 lsn 值在 redo 日志文件组中的偏移量;

2.4 lsn

(1)lsn(Log sequence number) 记录当前总共已经写入的日志量。初始值为 8704,其增长是随着字节数进行增长的(也包含log block header 和 trailer 的字节数,有点偏移量的感觉)

flushed_to_disk_lsn (Log flushed up to)表示刷新到磁盘中的 redo 日志量,初始时与 lsn 相同。

(2)buf_next_to_write 标记当前 log buffer 中已经有哪些日志被刷新到磁盘中。

(3)flush 链表中的脏页是按第一次修改发生的时间顺序进行排序的,即按照 oldest_modification 代表的 lsn 值进行排序,被多次更新的页面只需修改 newest_modification 属性值即可。

某个 MTR 执行结束后,会将修改的页对应的控制块加入 flush 链表的头部,若该控制块已经在 flush 链表中则只需修改 newest_modification。

在控制块中的两个属性:
oldest_modification :第一次修改该页面 MTR 开始时对应的 lsn 值。
newest_modification:修改该页面 MTR 结束时对应的 lsn 值。

(4)执行 checkpoint 的步骤

计算 flush 链表中表尾控制块中的 oldest_modification(Page flushed up to) ,小于该值的 lsn,说明其代表的脏页被刷新到磁盘中,是可以被覆盖的。并将该值赋值给 checkpoint_lsn;(Last checkpoint at);
根据 checkpoint_lsn 计算对应的 redo 日志文件组的偏移量,并记录到 checkpoint1 block 或者 checkpoint2 block 中(根据 checkpoint_no 奇偶决定)。

注意:Page flushed up to 和 Last checkpoint at 不一定相同,在执行 checkpoint 时相同,但可能后台进程会刷新 redo 日志到磁盘,就会改变 Page flushed up to。

2.5 崩溃恢复

比较 checkpoint1 block 和 checkpoint2 block 中的 checkpoint_no 确定哪个是最近的一个 checkpoint,取出对应的 checkpoint_lsn。

凡是小于 checkpoint_lsn 的日志都已经被刷新到磁盘中不需要进行恢复。

从 checkpoint_lsn 开始恢复到日志结尾,但日志中的记录可能都是不同页面交替写入的,会造成大量随机 IO。

(1)可以建立哈希表,将 space ID 和 page number 做为 key,使用开链法处理冲突。这样修改同一个页面的记录就会按先后顺序放在一个槽中,可以避免大量随机 IO;

(2)跳过已经刷新到磁盘中页面。每个页面中的 File Header 部分的 FIL_PAGE_LSN 属性,记录了其最近一次修改页面的 lsn 值,若当前日志的 lsn 值小于 FIL_PAGE_LSN ,也不需要进行恢复。

3. Undo log

对只读事务,只有它在第一次对某个用户创建的临时表执行增删改操作时,才会分配一个事务 ID。

对于读写事务,只有第一次对某个表执行增删改时,才会分配事务 ID。

事务 ID 的分配过程与隐藏列 row_id 大致相同。

3.1 Undo log 页面

页面类型为 FIL_PAGE_UNDO_LOG(0x0002),可以从系统表空间中分配,也可以从专门存放 undo log 的表空间中分配;

3.2 INSERT 操作对应的 undo log

InnoDB中 redo log 和 undo log_第4张图片
undo no:在一个事务中从 0 开始递增。

向表中插入记录时,实际上需要向聚簇索引和二级索引都插入相应记录,在回滚操作时,只需知道主键信息即可。(如何快速找到二级索引中主键对应记录?通过Change Buffer?)

InnoDB中 redo log 和 undo log_第5张图片

roll_pointer 是指向记录对应的 undo log 的指针

3.3 DELETE 操作对应的 undo log(类型为 TRX_UNDO_DEL_MARK_REC)

PAGE_FREE 指向由被删除记录组成的垃圾链表中的头节点。

删除一条记录会经历两个阶段:

  1. delete mark:仅仅将记录的 deleted_flag 标志位设为 1,然后修改 trx_id、roll_pointer 这些隐藏列的值。

  2. purge:删除语句的事务提交后,由专门的线程把该记录从正常记录的链表中移动到垃圾链表中,然后调整 PAGE_N_RECS、PAGE_LAST_INSERT、PAGE_FREE 等信息,注意这里插入采用头插法,即插入到垃圾链表的头结点处。

(1)在事务提交前,只会经历阶段 1,因此只需考虑删除操作在阶段 1 所做的影响进行回滚即可;

InnoDB中 redo log 和 undo log_第6张图片
这里的索引列信息主要在事务提交后使用(也包含聚簇索引),用来对中间状态的记录真正的删除(阶段 2);

pos 代表列的位置,表名是第几列,注意这里是包含隐藏列的。

3.4 UPDATE 操作对应的 undo log

(1)不更新主键(TRX_UNDO_UPD_EXIST_REC 类型)

  1. 就地更新(对每一个更新后的列与更新前占用存储空间一样大)
    直接在原纪录的基础上修改对应列的值

  2. 先删除旧记录,再插入新记录
    这里的删除操作是真正的删除操作,即阶段 1 和 2,特殊的是由用户线程同步执行真正的删除操作。

InnoDB中 redo log 和 undo log_第7张图片
如果有被更新的索引列,也会记录主键索引的信息。

(2)更新主键

将旧记录进行 delete mark 操作(会记录 TRX_UNDO_DEL_MARK_REC 类型的 undo log)

根据更新后各列的值创建一条新记录,并将其插入到聚簇索引中。(会记录 TRX_UNDO_INSERT_REC 类型的 undo log)

3.5 增删改操作对二级索引的影响

INSERT、DELETE 操作与聚簇索引类似

UPDATE 操作中若不涉及二级索引的列,则不用执行任何操作。否则,也需要对旧的二级索引记录执行 delete mark 操作,再根据更新后的值创建一条新的二级索引记录,然后在二级索引对应的 B+ 树中插入。

二级索引记录没有 trx_id、roll_point 等属性,每当增删改二级索引记录时,都会影响 Page Header 部分的 PAGE_MAX_TRX_ID 属性。

3.6 Undo 页面

(1)页面类型为 FIL_PAGE_UNDO_LOG,除了页面通用的 File Header \ Trailer 外,其中 Undo Page Header 如下
InnoDB中 redo log 和 undo log_第8张图片

  1. TYPE:undo log 的类型,分为两类:insert undo log 和 update undo log,分别使用十进制的 1、2 表示;类型为 TRX_UNDO_INSERT_REC 的 undo log 属于 insert undo log,其余类型都属于 update undo log;
    注意,不同大类的 undo log 不能混着存储,之所以这样划分是因为插入操作的 undo log 在事务提交后可以直接删除。

  2. START:第一条 undo log 在本页面的偏移量

  3. FREE:最后一条日志结束时的偏移量

  4. NODE:链表节点结构(前后指针,包含页号和偏移量)

(2)Undo 页面组成双向链表,链表中的第一个页面为 first undo page,其余为 normal undo page;

在一个事务执行过程中,可能需要 2 个 Undo 页面链表,一个为 insert undo 链表,另一个为 update undo 链表。

对普通表和临时表的记录进行改动时所产生的 undo log 要分别记录,因此一个事务最多需要 4 个链表。并非事务一开始就分配,而是使用到时才会进行分配。

(3)每个 Undo 页面链表都对应一个段,称为 Undo Log Segment。链表中的页面都是从这个段中申请的。

first undo page 中还包含一个 Undo Log Segment Header 部分,此部分包含了该链表对应段的 Segment Header 信息,具体如下:

InnoDB中 redo log 和 undo log_第9张图片

  1. STATE:本 Undo 页面链表的状态;
    TRX_UNDO_ACTIVE:有活跃事务正在向这个链表中写入 Undo 日志;
    TRX_UNDO_CACHED:被缓存,等待之后被其他事务重用;
    TRX_UNDO_TO_FREE:等待被释放,对 insert undo 链表,事务提交后就处于这种状态,不能被重用。
    TRX_UNDO_TO_PURGE:等待被 purge,对 update undo 链表,事务提交后就处于这种状态,不能被重用。
    TRX_UNDO_PREPARED:用于存储处于 PREPARE 阶段的事务产生的日志;
  2. LAST_LOG:本 Undo 页面链表中最后一个 Undo Log Header 的位置;
  3. FSEG_HEADER:本 Undo 页面链表对应段的 Segment Header 信息;
  4. PAGE_LIST:Undo 页面链表的基节点。

(4)同一个事务向一个 Undo 页面链表中写入的 undo 日志算一个组,存储这些组属性地方为 Undo Log Header;

first undo page 结构如下所示:
InnoDB中 redo log 和 undo log_第10张图片
Undo Log Header 结构如下所示:
InnoDB中 redo log 和 undo log_第11张图片

  1. TRX_ID:生成本组 undo log 的事务 ID;
  2. TRX_NO:事务提交后生成的序号;
  3. DEL_MARKS:标记本组 undo log 是否包含由 delete mark 操作产生的 undo 日志;
  4. LOG_START:本组 undo log 中第一条 undo log 在页面中的偏移量;
  5. XID_EXISTS:是否包含 XID 信息;
  6. DICT_TRANS:是不是由 DDL 语句产生的;
  7. TABLE_ID:如果 DICT_TRANS 为真,表示 DDL 操作的表的 ID;
  8. NEXT_LOG:下一组 undo log 在页面中开始的偏移量;(一般一个 undo 页面链表只有一个组,这里主要是考虑重用 undo log 链表的情况)
  9. PREV_LOG:上一组 undo log 在页面中开始的偏移量;
  10. HISTORY_NODE:表示 History 链表的节点;

3.7 重用 Undo 页面

一个 Undo 页面链表可被重用,需要满足条件:

该链表中只包含一个 Undo 页面;
该 Undo 页面已经使用的空间小于整个页面空间的 3/4;

(1)对 insert undo 链表
如果可重用,直接覆盖掉之前的 undo log,从头开始写入新事务的一组 undo log;需要适当修改 Undo Page Header 等中的信息。

(2)update undo

以 append 方式追加,注意,对每个新事务都需要一个单独的 Undo Log Header,也是以 append 方式追加,这也是 Undo Log Header 中 NEXT_LOG/PREV_LOG 属性的意义;

3.8 回滚段

每个事务最多可以拥有 4 个 Undo 页面链表,系统中可能有许多事务,为了更好的管理这些链表。

设计了一个 Rollback Segment Header 的页面类型,里面存放个跟 Undo 页面链表的 first undo page 的页号,这些页号称为 undo slot;

InnoDB中 redo log 和 undo log_第12张图片
每个 Rollback Segment Header 对应一个段,这个段称为回滚段,该段中只有一个页面;

  1. TRX_RSEG_MAX_SIZE:这个回滚段中所有 Undo 页面链表中的 Undo 页面数量之和的最大值,默认为 0xFFFFFFFE;
  2. HISTORY_SIZE:History 链表的页面数量;
  3. HISTORY:History 链表的基节点;
  4. FSEG_HEADER:Segment Header 结构,通过它可以找到该回滚段对应的 INODE Entry;
  5. UNDO_SLOTS:记录各链表的 first undo page 的页号;一个页号占用 4 字节,可以存储 1024 个 Undo 页面链表。
3.8.1 从回滚段申请 Undo 页面链表

(1)初始情况下,各 undo slot 被设置一个特殊值:FIL_NULL,表示不指向任何页面。
(2)有事务需要分配 Undo 页面链表时,会从回滚段的第一个 undo slot 开始检查:

  1. 若是 FIL_NULL,在表空间新建一个段(即 Undo Log Segment),从该段申请一个页面作为 Undo 页面链表的 first undo page (需要填写 Undo Log Segment Header,之后可以找到该段),把该页页号记录在这个 undo slot 处。
  2. 若不是 FIL_NULL,接着向下遍历;

(3)若这 1024 个 undo slot 都被占用,会停止执行该事务并返回错误;

(4)一个回滚段对应两个 cached 链表。如果有新事务需要分配 undo slot,都先从对应的 cached 链表中找,如果没有缓存,才会到回滚段的 Rollback Segment Header 页面中找。

当一个事务提交时,

  1. 如果其占用的 undo slot 指向的 Undo 页面链表符合被重用的条件,设置该链表为 TRX_UNDO_CACHED 状态,如果对应的 Undo 页面链表为 insert undo 链表,会被加入 insert undo cached 链表中,否则,加入 update undo cached 链表中。
  2. 不符合被重用的条件
    对 insert undo 链表,设置其状态为 TRX_UNDO_TO_FREE,会将 Undo 页面链表对应的段释放掉,把该 undo slot 设置为 FIL_NULL;
    对 update undo 链表,设置其状态为 TRX_UNDO_TO_PRUGE,把该 undo slot 设置为 FIL_NULL,将本事务写入的一组 undo log 放入 History 链表中;
3.8.2 多个回滚段

指向这些回滚段的指针(表 ID + 页号)存放在系统表空间中第 5 号页面中,即 TRX_SYS。共设计了 128 个回滚段。

3.8.3 回滚段的分类

(1)第 0 号回滚段必须在系统表空间中。33 ~ 127 号回滚段既可以在系统表空间中,也可以在自己配置的 undo 表空间中。

对普通表记录的修改,必须从此类段中分配相应的 undo slot;

(2)1 ~ 32 号回滚段必须在临时表空间中。

对临时表记录的修改,必须从此类段中分配相应的 undo slot;

(3)为什么区分开来?

向 undo 页面写入 undo 日志的本身也是一个写页面的过程,需要记录相应的 redo log,而针对临时表,不需要记录对应的 redo log;

3.8.4 roll_pointer

InnoDB中 redo log 和 undo log_第13张图片
is_insert:指向的日志是否为 TRX_UNDO_INSERT 的 undo log;
rseg_id:回滚段的编号(最多为 128 个回滚段,7 位足够表示);

3.8.5 事务分配 Undo 页面链表的详细过程
  1. 事务在执行时对普通表记录修改前,首先到系统表空间的第 5 号页面中分配一个回滚段
    采用循环使用方式分配回滚段,当前事务分配到第 0 号回滚段,下一个事务就分配 33 号回滚段,依次类推。
  2. 分配到回滚段后,首先查看该回滚段的 2 个 cached 链表是否有相应缓存。
  3. 如果没有缓存,需要在 Rollback Segment Header 页面中找一个可用的 undo slot 分配给当前事务;

(1)这里的问题是 cached 链表的基节点存储在哪里?

这里是在内存中维护了一组数据结构,并不在磁盘上。

参考下述网址:
http://mysql.taobao.org/monthly/2021/10/01/

你可能感兴趣的:(数据库,mysql,数据库)