Buffer Pool 的大小由系统变量 innodb_buffer_pool_size
控制,最小为 5MB 。
innodb_buffer_pool_size
的值小于 1GB 时,innodb_buffer_pool_instances
强制设置为 1。
(1)每一个缓冲页都有一些控制信息,包含该页所属的表空间 ID、页号、链表节点信息等。每个缓冲页对应的控制信息占用内存大小是相同的,称为控制块
(2)控制块与缓冲页一一对应,存放在 Buffer Pool 前面,innodb_buffer_pool_size
并不包含控制块占用的内存空间大小。
(1)链表的基节点包含头尾指针、当前链表节点数量,注意该基节点本身存储在另一块单独申请的内存空间中。
free 链表中实际内容为各控制块。
flush 链表存储的是被修改过的缓冲页。
LRU 链表优化:只有被访问的缓冲页位于 young 区域的后 1/4 页面时,才会被移动到 LRU 链表的头部。
(2)刷新脏页到磁盘:
(3)以 chunk 为单位向操作系统申请内存空间。innodb_buffer_pool_chunk_size
只能在服务器启动时指定。
通用结构为:type | space ID | page number | data
类型 | 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 之后赋值给全局变量;
在语句执行过程中,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 等,在进行恢复时,实际上是重播,即调用函数重新进行插入操作。
Mini-Transaction(MTR):对底层页面进行一次原子访问的过程。
(1)将日志分为若干组,这些组都是不可分割的(具有原子性,要么全恢复,要么一条也不恢复)
更新 Max Row ID 属性时产生的 redo 日志为一组
向聚簇索引对应的 B+ 树的页面中插入一条记录时产生的 redo 日志为一组
向二级索引对应的 B+ 树的页面中插入一条记录时产生的 redo 日志为一组
(2)有些需要保证原子性的操作生成多条 redo 日志,在该组最后一条 redo 日志后面加上一个特殊类型的 redo 日志(MLOG_MULTI_REC_END,该类型只有一个 type 字段(十进制的 31));
有些需要保证原子性的操作只生成一个 redo 日志(在该日志的 type 字段的最高位标识产生单个/一系列的 redo 日志)
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:校验和;
在内存中有个 buf_free
的全局变量指向后续写入的 redo 日志应该写到 log buffer 中的哪个 block 的哪个位置。
并不是每生成一条 redo 日志就将其插入到 log buffer 中,而是将每个 MTR 运行过程中产生的日志先暂存到某个地方,当 MTR 运行结束后,再将该组 redo 日志全部复制到 log buffer 中。
不同事务可能是并发执行的,因此不同事务的 MTR 可能是交替写入 log buffer 中。
log buffer 空间不足时
事务提交时
后台进程每个 1s 将 log buffer 中的 redo 日志刷新到磁盘
正常关闭服务器时
做 checkpoint 时
redo 日志文件也是由若干个 512 字节的 block 组成的。
前 4 个 block 存放一些管理信息。
LOG_HEADER_START_LSN:标记本 redo 文件偏移量为 2048 字节处对应的 lsn 值。
checkpoint1 和 2 的结构如下:
LOG_CHECKPOINT_NO:服务器执行 checkpoint 的编号;
LOG_CHECKPOINT_LSN:服务器在结束 checkpoint 时对应的 lsn 值,崩溃后恢复时从该值开始;
LOG_CHECKPOINT_OFFSET:上个属性中的 lsn 值在 redo 日志文件组中的偏移量;
(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。
比较 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 ,也不需要进行恢复。
对只读事务,只有它在第一次对某个用户创建的临时表执行增删改操作时,才会分配一个事务 ID。
对于读写事务,只有第一次对某个表执行增删改时,才会分配事务 ID。
事务 ID 的分配过程与隐藏列 row_id 大致相同。
页面类型为 FIL_PAGE_UNDO_LOG(0x0002),可以从系统表空间中分配,也可以从专门存放 undo log 的表空间中分配;
向表中插入记录时,实际上需要向聚簇索引和二级索引都插入相应记录,在回滚操作时,只需知道主键信息即可。(如何快速找到二级索引中主键对应记录?通过Change Buffer?)
roll_pointer 是指向记录对应的 undo log 的指针
PAGE_FREE 指向由被删除记录组成的垃圾链表中的头节点。
删除一条记录会经历两个阶段:
delete mark:仅仅将记录的 deleted_flag 标志位设为 1,然后修改 trx_id、roll_pointer 这些隐藏列的值。
purge:删除语句的事务提交后,由专门的线程把该记录从正常记录的链表中移动到垃圾链表中,然后调整 PAGE_N_RECS、PAGE_LAST_INSERT、PAGE_FREE 等信息,注意这里插入采用头插法,即插入到垃圾链表的头结点处。
(1)在事务提交前,只会经历阶段 1,因此只需考虑删除操作在阶段 1 所做的影响进行回滚即可;
这里的索引列信息主要在事务提交后使用(也包含聚簇索引),用来对中间状态的记录真正的删除(阶段 2);
pos 代表列的位置,表名是第几列,注意这里是包含隐藏列的。
(1)不更新主键(TRX_UNDO_UPD_EXIST_REC 类型)
就地更新(对每一个更新后的列与更新前占用存储空间一样大)
直接在原纪录的基础上修改对应列的值
先删除旧记录,再插入新记录
这里的删除操作是真正的删除操作,即阶段 1 和 2,特殊的是由用户线程同步执行真正的删除操作。
(2)更新主键
将旧记录进行 delete mark 操作(会记录 TRX_UNDO_DEL_MARK_REC 类型的 undo log)
根据更新后各列的值创建一条新记录,并将其插入到聚簇索引中。(会记录 TRX_UNDO_INSERT_REC 类型的 undo log)
INSERT、DELETE 操作与聚簇索引类似
UPDATE 操作中若不涉及二级索引的列,则不用执行任何操作。否则,也需要对旧的二级索引记录执行 delete mark 操作,再根据更新后的值创建一条新的二级索引记录,然后在二级索引对应的 B+ 树中插入。
二级索引记录没有 trx_id、roll_point 等属性,每当增删改二级索引记录时,都会影响 Page Header 部分的 PAGE_MAX_TRX_ID 属性。
(1)页面类型为 FIL_PAGE_UNDO_LOG,除了页面通用的 File Header \ Trailer 外,其中 Undo Page Header 如下
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 在事务提交后可以直接删除。
START:第一条 undo log 在本页面的偏移量
FREE:最后一条日志结束时的偏移量
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 信息,具体如下:
(4)同一个事务向一个 Undo 页面链表中写入的 undo 日志算一个组,存储这些组属性地方为 Undo Log Header;
first undo page 结构如下所示:
Undo Log Header 结构如下所示:
一个 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 属性的意义;
每个事务最多可以拥有 4 个 Undo 页面链表,系统中可能有许多事务,为了更好的管理这些链表。
设计了一个 Rollback Segment Header 的页面类型,里面存放个跟 Undo 页面链表的 first undo page 的页号,这些页号称为 undo slot;
每个 Rollback Segment Header 对应一个段,这个段称为回滚段,该段中只有一个页面;
(1)初始情况下,各 undo slot 被设置一个特殊值:FIL_NULL,表示不指向任何页面。
(2)有事务需要分配 Undo 页面链表时,会从回滚段的第一个 undo slot 开始检查:
(3)若这 1024 个 undo slot 都被占用,会停止执行该事务并返回错误;
(4)一个回滚段对应两个 cached 链表。如果有新事务需要分配 undo slot,都先从对应的 cached 链表中找,如果没有缓存,才会到回滚段的 Rollback Segment Header 页面中找。
当一个事务提交时,
指向这些回滚段的指针(表 ID + 页号)存放在系统表空间中第 5 号页面中,即 TRX_SYS。共设计了 128 个回滚段。
(1)第 0 号回滚段必须在系统表空间中。33 ~ 127 号回滚段既可以在系统表空间中,也可以在自己配置的 undo 表空间中。
对普通表记录的修改,必须从此类段中分配相应的 undo slot;
(2)1 ~ 32 号回滚段必须在临时表空间中。
对临时表记录的修改,必须从此类段中分配相应的 undo slot;
(3)为什么区分开来?
向 undo 页面写入 undo 日志的本身也是一个写页面的过程,需要记录相应的 redo log,而针对临时表,不需要记录对应的 redo log;
is_insert:指向的日志是否为 TRX_UNDO_INSERT 的 undo log;
rseg_id:回滚段的编号(最多为 128 个回滚段,7 位足够表示);
(1)这里的问题是 cached 链表的基节点存储在哪里?
这里是在内存中维护了一组数据结构,并不在磁盘上。
参考下述网址:
http://mysql.taobao.org/monthly/2021/10/01/