1.数据页的基本介绍
数据页的大小是16kb,有很多种数据页。
比如存放表空间头部信息的页,存放Insert Buffer
信息的页,存放INODE
信息的页,存放undo
日志信息的页等。
名称 | 中文名 | 占用空间大小 | 简单描述 |
---|---|---|---|
File Header | 文件头部 | 38 字节 | 页的一些通用信息 |
Page Header | 页面头部 | 56 字节 | 数据页专有的一些信息 |
Infimum + Supremum | 最小记录和最大记录 | 26 字节 | 两个虚拟的行记录 |
User Records | 用户记录 | 不确定 | 实际存储的行记录内容 |
Free Space | 空闲空间 | 不确定 | 页中尚未使用的空间 |
Page Directory | 页面目录 | 不确定 | 页中的某些记录的相对位置 |
File Trailer | 文件尾部 | 8 字节 | 校验页是否完整 |
我们自己存储的记录会按照我们指定的行格式
存储到User Records
部分。
我们插入记录会按照指定的行格式来插入进去。刚开始并没有User Records这部分。
每当我们插入一条记录,都会从Free Space
部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records
部分,当Free Space
部分的空间全部被User Records
部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。
如果我们将某一个列指定为主键的话,在具体的行格式中InnoDB就没必要为我们去创建那个所谓的 row_id 隐藏列了。
上一篇笔记中,行格式记录,实际上就是每一条记录都有的信息。
其中记录头信息是有这5个字节,40位的信息。
记录头信息
中各个属性的大体意思浏览一下(使用Compact
行格式):
名称 | 大小(单位:bit) | 描述 |
---|---|---|
预留位1 | 1 | 没有使用 |
预留位2 | 1 | 没有使用 |
delete_mask | 1 | 标记该记录是否被删除 |
min_rec_mask | 1 | B+树的每层非叶子节点中的最小记录都会添加该标记 |
n_owned | 4 | 表示当前记录拥有的记录数 |
heap_no | 13 | 表示当前记录在记录堆的位置信息 |
record_type | 3 | 表示当前记录的类型,0 表示普通记录,1 表示B+树非叶节点记录,2 表示最小记录,3 表示最大记录 |
next_record | 16 | 表示下一条记录的相对位置 |
TIPS:
- B+树的非叶子节点记录,也是有标识的。
- 记录中的数据应该是以二进制存在的。
- 各条记录在
User Records
中存储的时候并没有空隙。 - delete_mask 标记着当前记录是否被删除,占用1个二进制位,值为
0
的时候代表记录并没有被删除,为1
的时候代表记录被删除掉了。被删除的记录还在页中!!这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后把其他的记录在磁盘上重新排列需要性能消耗,所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的垃圾链表
,在这个链表中的记录占用的空间称之为所谓的可重用空间
,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。 - 这个delete_mask位设置为1和将被删除的记录加入到垃圾链表中其实是两个阶段
-
min_rec_mask
B+树的每层非叶子节点中的最小记录都会添加该标记 -
heap_no
这个属性表示当前记录在本
页
中的位置,从图中可以看出来,我们插入的4条记录在本页
中的位置分别是:2
、3
、4
、5
。是不是少了点啥?heap_no
值为0
和1
的记录,这两个记录并不是我们自己插入的,所以有时候也称为伪记录
或者虚拟记录
。这两个伪记录一个代表最小记录
,一个代表最大记录
-
记录也可以比大小,对于一条完整的记录来说,比较记录的大小就是比较
主键
的大小。
两条伪记录分别为最小记录与最大记录(跟图上的Infimum + supremum 有关)。这两条记录的构造十分简单,都是由5字节大小的记录头信息
和8字节大小的一个固定的部分组成的,如图所示
它们并不存放在页
的User Records
部分,他们被单独放在一个称为Infimum + Supremum
的部分
next_record
表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。
比方说第一条记录的next_record
值为32
,意味着从第一条记录的真实数据的地址处向后找32
个字节便是下一条记录的真实数据。
其实就是个链表。
下一条记录
指得并不是按照我们插入顺序的下一条记录,而是按照主键值由小到大的顺序的下一条记录。
所以下一条记录并不一定物理连续,
Infimum记录(也就是最小记录) 的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录)
是一个链表形势的把数据穿插起来。
删除记录的话,就是相当于链表的next record的指针移动(偏移量的变化),实际上物理上面并没有移除。
并且被删除的记录next record为0;
不论我们怎么对页中的记录做增删改操作,InnoDB始终会维护一条记录的单链表,链表中的各个节点是按照主键值由小到大的顺序连接起来的。
next_record这个指针为什么要指向记录头信息和真实数据之间的位置呢?
因为这个位置刚刚好,向左读取就是记录头信息,向右读取就是真实数据。我们前边还说过变长字段长度列表、NULL值列表中的信息都是逆序存放,这样可以使记录中位置靠前的字段和它们对应的字段长度信息在内存中的距离更近,可能会提高高速缓存的命中率。
大喊牛逼,感觉意思就是因为之前的额外信息是逆序存储的,那么从中间进行向左推遍历,就能正序的访问列的变长大小之类的了。而且,这样也比较好的向右寻找到对应真实数据。左右相对对称,
而且,如果删除的记录被标记了删除,存储空间没有回收的话,再次插入主键相同的,会进行复用之前的物理空间
当数据页中存在多条被删除掉的记录时,这些记录的next_record属性将会把这些被删除掉的记录组成一个垃圾链表,以备之后重用这部分存储空间。
2. Page Header(页面头部)
数据页的第二部分!!!占56个字节,存储各种状态信息。
这是为了得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header
的部分,它是页
结构的第二部分,这个部分占用固定的56
个字节,专门存储各种状态信息,具体各个字节都是干嘛的看下表:
名称 | 占用空间大小 | 描述 |
---|---|---|
PAGE_N_DIR_SLOTS | 2 字节 | 在页目录中的槽数量 |
PAGE_HEAP_TOP | 2 字节 | 还未使用的空间最小地址,也就是说从该地址之后就是Free Space |
PAGE_N_HEAP | 2 字节 | 本页中的记录的数量(包括最小和最大记录以及标记为删除的记录) |
PAGE_FREE | 2 字节 | 第一个已经标记为删除的记录地址(各个已删除的记录通过next_record 也会组成一个单链表,这个单链表中的记录可以被重新利用) |
PAGE_GARBAGE | 2 字节 | 已删除记录占用的字节数 |
PAGE_LAST_INSERT | 2 字节 | 最后插入记录的位置 |
PAGE_DIRECTION | 2 字节 | 记录插入的方向 |
PAGE_N_DIRECTION | 2 字节 | 一个方向连续插入的记录数量 |
PAGE_N_RECS | 2 字节 | 该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录) |
PAGE_MAX_TRX_ID | 8 字节 | 修改当前页的最大事务ID,该值仅在二级索引中定义 |
PAGE_LEVEL | 2 字节 | 当前页在B+树中所处的层级 |
PAGE_INDEX_ID | 8 字节 | 索引ID,表示当前页属于哪个索引 |
PAGE_BTR_SEG_LEAF | 10 字节 | B+树叶子段的头部信息,仅在B+树的Root页定义 |
PAGE_BTR_SEG_TOP | 10 字节 | B+树非叶子段的头部信息,仅在B+树的Root页定义 |
这里面比较需要注意的:
PAGE_DIRECTION
假如新插入的一条记录的主键值比上一条记录的主键值大,我们说这条记录的插入方向是右边,反之则是左边。用来表示最后一条记录插入方向的状态就是PAGE_DIRECTION
。(比主键大不大,决定了插入方向)
PAGE_N_DIRECTION
假设连续几次插入新记录的方向都是一致的,InnoDB
会把沿着同一个方向插入记录的条数记下来,这个条数就用PAGE_N_DIRECTION
这个状态表示。当然,如果最后一条记录的插入方向改变了的话,这个状态的值会被清零重新统计。
3. File Header(文件头部)
刚才的Page Header是只针对数据页才有的各种状态(比如记录多少个数据)
这个文件头是mysql中,各种页的都通用的内容。
页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁(38个字节)
名称 | 占用空间大小 | 描述 |
---|---|---|
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 字节 | 页属于哪个表空间 |
FIL_PAGE_SPACE_OR_CHKSUM(校验和)
这个代表当前页面的校验和(checksum)。啥是个校验和?就是对于一个很长很长的字节串来说,我们会通过某种算法来计算一个比较短的值来代表这个很长的字节串,这个比较短的值就称为校验和
。这样在比较两个很长的字节串之前先比较这两个长字节串的校验和,如果校验和都不一样两个长字节串肯定是不同的,所以省去了直接比较两个比较长的字节串的时间损耗。
FIL_PAGE_OFFSET(唯一编号)
每一个页
都有一个单独的页号,就跟你的身份证号码一样,InnoDB
通过页号来可以唯一定位一个页
。
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 | 溢出页 |
FIL_PAGE_INDEX | 0x45BF | 索引页,也就是我们所说的数据页 |
FIL_PAGE_PREV
和FIL_PAGE_NEXT
上一个页号和下一个页号,类似双向链表,把页穿起来了。不需要物理关联。
并不是所有类型的页都有上一个和下一个页的属性,不过我们本集中唠叨的数据页
(也就是类型为FIL_PAGE_INDEX
的页)是有这两个属性的,所以所有的数据页其实是一个双链表
感谢大佬得书!!https://juejin.im/book/5bffcbc9f265da614b11b731/section/5bffdb30518825773a2ed38c