参考https://blog.csdn.net/xioayu96/article/details/107857452
页(Page)是 Innodb 存储引擎用于管理数据的最小磁盘单位(默认16K)
InnoDB的数据页有很多种,比如,索引页,Undo页,Inode页,系统页,BloB页等+
参考数据库
id | 姓名 | 年龄 |
1 | 赵 | 10 |
2 | 钱 | 11 |
3 | 孙 | 12 |
4 | 李 | 13 |
5 | 周 | 14 |
6 | 吴 | 11 |
7 | 郑 | 13 |
8 | 王 | 12 |
CREATE TABLE `test` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` char(1) NOT NULL DEFAULT '',
`age` tinyint(3) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
1.数据页大小
磁盘储存单位:扇区(512byte)
文件系统:块(4kb)
InnoDB:页(16kb,默认)
2.结构
2.1结构
数据页结构 | ||||||
File Header | Page Header | Infimun + Supremum | User Recorders | free Space | Page Directory | FileTrailer |
38字节 | 56字节 | 26(13+13)字节 | 不确定 | 不确定 | 不确定 | 8字节 |
2.2说明
名称 | 中文名 | 占用空间大小 | 简单描述 |
File Header | 文件头部 | 38字节 | 页的一些通用信息 |
Page Header | 页面头部 | 56字节 | 数据页专有的一些信息 |
Infimum + Supremum | 最小记录和最大记录 | 26字节 | 两个虚拟的行记录 |
User Records | 用户记录 | 不确定 | 实际存储的行记录内容 |
Free Space | 空闲空间 | 不确定 | 页中尚未使用的空间 |
Page Directory | 页面目录 | 不确定 | 页中的某些记录的相对位置 |
File Trailer | 文件尾部 | 8字节 | 校验页是否完整 |
2.3插入流程
(1)无数据(不存在)
数据页结构 | ||||||||
File Header | Page Header | Infimun+ Supremum | free Space | Page Directory | FileTrailer | |||
…… |
(2)插入1条数据
数据页结构 | ||||||||
File Header | Page Header | Infimun+ Supremum | User Recorders | free Space | Page Directory | FileTrailer | ||
记录1 | …… |
(3)插入2条数据
数据页结构 | ||||||||
File Header | Page Header | Infimun+ Supremum | User Recorders | free Space | Page Directory | FileTrailer | ||
记录1 | 记录2 | …… |
(4)插入N条数据
数据页结构 | ||||||||
File Header | Page Header | Infimun+ Supremum | User Recorders | free Space | Page Directory | FileTrailer | ||
记录1 | 记录2 | …… | …… |
3.File Header 结构和作用
名称 | 占用空间大小 | 描述 |
FIL_PAGE_SPACE_OR_CHKSUM | 4字节 | 页的校验和(checksum值) |
FIL_PAGE_OFFSET | 4字节 | 页号 |
FIL_PAGE_PREV | 4字节 | 上一个页的页号 |
FIL_PAGE_NEXT | 4字节 | 下一个页的页号 |
FIL_PAGE_LSN | 8字节 | 页面被最后修改时对应的日志序列位置(英文名是:Log Sequence Number) |
FIL_PAGE_TYPE | 2字节 | 该页的类型 |
FIL_PAGE_FILE_FLUSH_LSN | 8字节 | 仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值(数据更新版本) |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4字节 | 页属于哪个表空间 |
3.1 FIL_PAGE_SPACE_OR_CHKSUM
这个代表当前页面的校验和(checksum)。啥是个校验和?就是对于一个很长很长的字节串来说,我们会通过某种算法来计算一个比较短的值来代表这个很长的字节串,这个比较短的值就称为校验和。这样在比较两个很长的字节串之前先比较这两个长字节串的校验和,如果校验和都不一样两个长字节串肯定是不同的,所以省去了直接比较两个比较长的字节串的时间损耗。
3.2 FIL_PAGE_OFFSET
每一个页都有一个单独的页号,就跟你的身份证号码一样,InnoDB通过页号来可以唯一定位一个页。
3.3 FIL_PAGE_TYPE
这个代表当前页的类型,我们前边说过,InnoDB为了不同的目的而把页分为不同的类型,我们上边介绍的其实都是存储记录的数据页,其实还有很多别的类型的页,具体如下表:
类型名称 | 十六进制 | 描述 |
FIL_PAGE_TYPE_ALLOCATED | 0x0000 | 最新分配,还没使用 |
FIL_PAGE_UNDO_LOG | 0x0002 | Undo日志页 |
FIL_PAGE_INODE | 0x0003 | 段信息节点 |
FIL_PAGE_IBUF_FREE_LIST | 0x0004 | Insert Buffer空闲列表 |
FIL_PAGE_IBUF_BITMAP | 0x0005 | Insert Buffer位图 |
FIL_PAGE_TYPE_SYS | 0x0006 | 系统页 |
FIL_PAGE_TYPE_TRX_SYS | 0x0007 | 事务系统数据 |
FIL_PAGE_TYPE_FSP_HDR | 0x0008 | 表空间头部信息 |
FIL_PAGE_TYPE_XDES | 0x0009 | 扩展描述页 |
FIL_PAGE_TYPE_BLOB | 0x000A | BLOB页 |
FIL_PAGE_INDEX | 0x45BF | 索引页,也就是我们所说的数据页 |
我们存放记录的数据页的类型其实是FIL_PAGE_INDEX,也就是所谓的索引页。
3.4 FIL_PAGE_PREV和FIL_PAGE_NEXT
我们前边强调过,InnoDB都是以页为单位存放数据的,有时候我们存放某种类型的数据占用的空间非常大(比方说一张表中可以有成千上万条记录),InnoDB可能不可以一次性为这么多数据分配一个非常大的存储空间,如果分散到多个不连续的页中存储的话需要把这些页关联起来,FIL_PAGE_PREV和FIL_PAGE_NEXT就分别代表本页的上一个和下一个页的页号。这样通过建立一个双向链表把许许多多的页就都串联起来了,而无需这些页在物理上真正连着。需要注意的是,并不是所有类型的页都有上一个和下一个页的属性,不过我们本集中唠叨的数据页(也就是类型为FIL_PAGE_INDEX的页)是有这两个属性的,所以所有的数据页其实是一个双链表。
就像这样:
3.5FIL_PAGE_FILE_FLUSH_LSN
- ibdata第一个块FIL中的lsn(FIL_PAGE_FILE_FLUSH_LSN):ibdata的26后面8字节是在innodb 干净关闭的时候进行更新的,如果不正常关闭不会进行写入(FIL_PAGE_FILE_FLUSH_LSN)
- redolog中MLOG_CHECKPOINT的lsn: redolog MLOG_CHECKPOINT lsn的写入是在每次checkpoint的时候同步写入的.干净关闭会更新。
- redolog header中的lsn:是在每次checkpoint的时候异步写入的在MLOG_CHECKPOINT写入之后.干净关闭会更新。
我们表示为lsn1/lsn2/lsn3
正常关闭3个lsn是相等的,如果非正常关闭innodb,lsn1不会更新,因此lsn3必然不和lsn1相等,则判定需要进行carsh recovery
4.Page Header 结构和作用
名称 | 占用空间大小 | 备注 |
PAGE_N_DIR_SLOTS | 2 | 页面目录部分中目录插槽的数量;初始值= 2 |
PAGE_HEAP_TOP | 2 | 指向堆中第一条记录的记录指针 |
PAGE_N_HEAP | 2 | 堆记录数;初始值= 2 |
PAGE_FREE | 2 | 记录指向第一个空闲记录的指针 |
PAGE_GARBAGE | 2 | “已删除记录中的字节数” |
PAGE_LAST_INSERT | 2 | 指向最后插入的记录的记录指针 |
PAGE_DIRECTION | 2 | 要么PAGE_LEFT, PAGE_RIGHT或 PAGE_NO_DIRECTION |
PAGE_N_DIRECTION | 2 | 相同方向上连续插入的数量,例如“最后5个都在左侧” |
PAGE_N_RECS | 2 | 用户记录数 |
PAGE_MAX_TRX_ID | 8 | 可能更改了页面记录的交易的最高ID(仅为二级索引设置) |
PAGE_LEVEL | 2 | 索引内的级别(叶子页为0) |
PAGE_INDEX_ID | 8 | 页面所属索引的标识符 |
PAGE_BTR_SEG_LEAF | 10 | “ B树中的叶子页的文件段标题”(与此处无关) |
PAGE_BTR_SEG_TOP | 10 | “ B树中非叶子页面的文件段头”(与此处无关) |
一些页面标题部分需要进一步说明:
4.1 PAGE_FREE :
已释放(由于删除或迁移)的记录位于单向链接列表中。PAGE_FREE 页面页眉中的指针指向列表中的第一条记录。记录标题中的“下一个”指针(特别是在记录的“额外字节”中)指向列表中的下一个记录。
4.2 PAGE_DIRECTION和 PAGE_N_DIRECTION:
知道插入片段是否以不断递增的顺序出现很有用。那会影响 InnoDB效率。
4.3 PAGE_HEAP_TOP和 PAGE_FREE和 PAGE_LAST_INSERT:
警告:像所有记录指针一样,它们并非指向记录的开头,而是指向其起源(请参见前面对记录结构的讨论)。
4.4 PAGE_BTR_SEG_LEAF和 PAGE_BTR_SEG_TOP:
这些变量包含有关索引节点文件段的信息(空间ID,页号和字节偏移)。 InnoDB使用该信息来分配新页面。有两个不同的变量,因为它们 InnoDB分别为叶页和上级页分配。
5. User Recorders行组织方式
Infimun行:最小行(开始行)
delete_mask | min_rec_mask | n_owned | heap_no | record_type | next_record (第一条记录相对地址) | Infimun |
第一条数据(id = 1)
delete_mask | min_rec_mask | n_owned | heap_no | record_type | next_record (第二条记录相对地址) | 1 | 赵 | 10 |
第二条数据(id = 3)
delete_mask | min_rec_mask | n_owned | heap_no | record_type | next_record (第三条记录相对地址) | 2 | 钱 | 11 |
第N条数据(id = n)
delete_mask | min_rec_mask | n_owned | heap_no | record_type | next_record (Supremum相对地址) | n | 无 | 100 |
Supremum行:最大行(结束行)
delete_mask | min_rec_mask | n_owned | heap_no | record_type | next_record (此处值为0) | Supremum |
5.1 图示
5.2 删除第二条记录
从图中可以看出来,删除第2条记录前后主要发生了这些变化:
- 第2条记录并没有从存储空间中移除,而是把该条记录的delete_mask值设置为1。
- 第2条记录的next_record值变为了0,意味着该记录没有下一条记录了。
- 第1条记录的next_record指向了第3条记录。
- 还有一点你可能忽略了,就是最大记录的n_owned值从5变成了4,关于这一点的变化我们稍后会详细说明的。
5.3 添加(id=2)
从图中可以看到,InnoDB并没有因为新记录的插入而为它申请新的存储空间,而是直接复用了原来被删除记录的存储空间。
6. Page Directory目录(插槽)
将所有正常的记录划分为几个组,每个组的最后一条记录头信息中的n_owned属性表示所在组的记录数,将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到Page Directory中。Page Directory中的偏移量称为slot(槽)。每个槽位占用两个字节,
InnoDB没有页面中每条记录的插槽。相反,它保留一个稀疏目录
6.1 分组记录数(n_owned):
最小记录行分组(Infimun):1
最大记录行分组(Supremum):1-8
普通分组:4-8
其中最小记录所在的组只能有一条记录也就是它自己,最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间
6.2 分组步骤:
(1)数据页开始时:只有最小值和最大值2个分组
(2)添加数据时:先查找槽
初始情况,只有最小记录和最大记录2个分组
插记录时先查找槽,将对应的记录(槽指向的数据)n_owned加1
组中的记录满时(记录值达到8),将组一分为二,同时新增一个槽
6.2.1 插入过程
插入4条数据,此时存在6(4+2)条数据
- 初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
- 之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
- 在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。
6.2.2 查找过程
插入16条数据,此时存在18(16+2)条数据
现在看怎么从这个页目录中查找记录。因为各个槽代表的记录的主键值都是从小到大排序的,所以我们可以使用所谓的二分法来进行快速查找。4个槽的编号分别是:0、1、2、3、4,所以初始情况下最低的槽就是low=0,最高的槽就是high=4。比方说我们想找主键值为6的记录,过程是这样的:
- 计算中间槽的位置:(0+4)/2=2,所以查看槽2对应记录的主键值为8,又因为8 > 6,所以设置high=2,low保持不变。
- 重新计算中间槽的位置:(0+2)/2=1,所以查看槽1对应的主键值为4,又因为4 < 6,所以设置low=1,high保持不变。
- 因为high - low的值为1,所以确定主键值为6的记录在槽2对应的组中。此刻我们需要找到槽2中主键值最小的那条记录,然后沿着单向链表遍历槽2中的记录。但是我们前边又说过,每个槽对应的记录都是该组中主键值最大的记录,这里槽2对应的记录是主键值为8的记录,怎么定位一个组中最小的记录呢?别忘了各个槽都是挨着的,我们可以很轻易的拿到槽1对应的记录(主键值为4),该条记录的下一条记录就是槽2中主键值最小的记录,该记录的主键值为5。所以我们可以从这条主键值为5的记录出发,遍历槽2中的各条记录,直到找到主键值为6的那条记录即可。由于一个组中包含的记录条数只能是1~8条,所以遍历一个组中的记录的代价是很小的。
所以在一个数据页中查找指定主键值的记录的过程分为两步:
- 通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录。
- 通过记录的next_record属性遍历该槽所在的组中的各个记录。
7. File Trailer
我们知道InnoDB存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以页为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据同步到磁盘中。但是在同步了一半的时候中断电了咋办,这不是莫名尴尬么?为了检测一个页是否完整(也就是在同步的时候有没有发生只同步一半的尴尬情况),InnoDB的设计师在每个页的尾部都加了一个File Trailer部分,这个部分由8个字节组成,可以分成2个小部分:
- 前4个字节代表页的校验和
这个部分是和File Header中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在File Header中的校验和就代表着已经修改过的页,而在File Trialer中的校验和代表着原先的页,二者不同则意味着同步中间出了错。
- 后4个字节代表页面被最后修改时对应的日志序列位置(LSN)
这个部分也是为了校验页的完整性的