页是InnoDB管理存储空间的基本单位,通常一个页的大小是16KB,InnoDB为了不同的目的设计了多种类型的页,本篇主要学习数据页。
数据页结构
名称 | 占用空间 | 描述 |
---|---|---|
File Header | 38字节 | 页的一些通用信息 |
Page Header | 56字节 | 数据页的一些专有信息 |
Infimum+supremum | 26字节 | 两条虚拟的记录,页中最小记录与最大记录 |
User Records | - | 存储用户记录的空间 |
Free Space | - | 页中未使用空间 |
Page Directory | - | 页面中某些记录的相对位置 |
File Trailer | 8字节 | 校验页是否完整 |
记录在数据页中的存储
User Records主要是用来存储用户的真实数据的部分,存储数据的格式就是上一篇提到的行记录格式;但是在页刚开始生成的时候User Records的空间是空,当Mysql需要插入的时候, 会从Free Space分配数据记录大小的空间,并将这个空间划分到User Records,当Free Space被完全使用完成后表示该数据页已经使用完成,需要重新申请新的数据页。
数据记录头信息
delete_flag: 这个属性用来标记当前记录是否被删除,占用1个位,0表示未删除,1表示该记录被删除;被删除的记录只是用该字段标记,依旧存储在磁盘上,之所以这样设计, 是因为移除他们后还需要对其他记录做整理,这样会有性能损耗;所以只需要打上标记,然后把被删除的记录组成一个垃圾链表,这个链表占用的空间就是可重用空间。之后新插入的数据会覆盖这些被删除的记录
heap_no: 当我们把数据存放到数据页的User Records中是,数据记录本质上是一条接着一条紧密排列存放着,InnoDB把这个紧密排列的空间称作堆,为了方便管理,每条记录都会保存当前记录在堆中的 相对位置,这就是heap_no
用户插入数据的heap_no是从2开始依次递增,如图中记录的heap_no就应该是:2、3、4、5、6; 为什么使用2开始增加,而不是从0开始增加呢?这是应为InnoDB插入了两条虚拟的记录,Infimum和Supremum, 其中Infimum表示UserRecords中最小的记录,heap_no=1; Supremum表示最大的记录heap_no=2。
Infimum和Supremum的数据结构很简单,5个字节的头信息和8个字节大小的固定单词组成。
堆中的记录一旦分配了heap_no,就不会再发生改变,即使记录行被删除,heap_no也不会变
- record_type: 记录的类型字段,0:用户插入的普通记录; 1:B+树非叶子节点的目录项记录;2:infimum记录; 3:supremum记录
- next_record: 表示从当前数据指向下一条记录真实数据的位置;这样就把所有的数据记录串联成了一条链表,这条链表不是按照插入顺序链接的,而是按照主键大小链接
记录按照主键大小由小到大顺序组成了一条单向链表,supremum记录的next_record=0表示supremum之后没有下一条记录了。 假如我们需要删除掉第二条记录:
- 把第二条记录delete_flag设置成1, next_record设置成0 ,表示没有下一条记录了
- 把第一条的next_record指向第三条记录
为什么next_record指向了头信息和真实数据之间?为啥不直接指向记录的开始位置?
这是因为这个位置刚好,向左可以读取到头信息以及变长字段列表,向右可以之间读取到真实数据,所以这里可以解释前一篇中可变字段列表和Null值列表为啥到逆序存放;
并且因为局部性原理,所以指向这里可以提高高速缓冲的命中率
Page Directory (页目录)
现在,我们知道了记录在页中是按照主键值由小到大顺序串联成一条单向链表,如果我们根据主键查找某一条记录,因为怎么查找? 最简单的方式就是遍历这条链表,最终都能够查询到需要的数据,但是这样查询效率不会太高
为了解决这个问题,InnoDB引入了页目录,具体的操作:
- 将所有的数据记录划分为几个组,包括infimum记录和supremum记录,不包括已经删除的数据
- 每个组最后一条记录作为这个组的组长,组内其余的记录都是组员,组长的 n_owned 属性会记录该组共有多少条记录
- 把每个组的组长在页面中的地址偏移量提取出来按照顺序存储到数据页的尾部,页目录中存放到这些偏移量称为Slot,每个Slot占用2个字节,页目录就是由多个slot组成。
页目录构建好了之后,再来看看通过主键查询这个场景,这时候就可以通过页目录使用折半查找查找记录
InnoDB对每个组中数据条数由规定:infimum记录所在的组只能有1条,supremum记录所在的只有有1~8条,其他组的条数范围只能是4~5;所以当向一个组插入一条数据的时候, 如果组的总条数大于了8条,就会把这个组拆分成两个组4条、5条
Page Header
InnoDB为了得到存储在数据页中记录的状态信息,比如:数据页中已经存储了多少条记录,FreeSpace的地址偏移量,页面目录存储了多少Slot等, 所以在数据页中单独开辟了一段空间来存储这些信息,这就是Page Header, 总共占用了56个字节
名称 | 占用空间 | 描述 |
---|---|---|
page_n_dir_slots | 2字节 | 在页目录中的slot数量 |
page_heap_top | 2字节 | 还未使用的空间最小地址,也就是说从该地址开始之后就是free space |
page_n_heap | 2字节 | 第一位表示本记录是否为紧凑型,剩余的15位记录本页堆中存储的总记录条数(包括infimum,supremum,以及已经删除的记录) |
page_free | 2字节 | 已经被删除的记录会通过一条单向链表链接在一起,这条链表占用的空间可以被重用,page_free记录的就是这条链表的头节点地址偏移量 |
page_garbage | 2字节 | 已删除记录占用的字节数 |
page_last_insert | 2字节 | 最后插入记录的位置 |
page_direction | 2字节 | 记录插入的方向 |
page_n_direction | 2字节 | 一个方向连续插入的记录数量 |
page_n_recs | 2字节 | 该页中用户的记录数量(不包括infimum,supremum,以及已经删除的记录) |
page_max_trx_id | 8字节 | 修改当页的最大事物id,仅在二级索引中定义 |
page_level | 2字节 | 当前页在B+树中所处的层级 |
page_index_id | 8字节 | 记录当前页属于哪个索引 |
page_btr_seq_leaf | 10字节 | B+树叶子节点的头部信息,仅在B+树的根页面中定义 |
page_btr_seq_top | 10字节 | B+树非叶子节点的头部信息,仅在B+树的根页面中定义 |
File Header
前面提到的Page Header主要记录的是页内数据的状态信息,而File Header记录的是页的通用信息,比如:页号是多少,上一页及下一页的地址是多少,共占用38个字节
名称 | 占用空间 | 描述 |
---|---|---|
file_page_space_or_chksum | 4字节 | 记录页的校验和 |
file_page_offset | 4字节 | 页号 |
file_page_prev | 4字节 | 上一页页号 |
file_page_next | 4字节 | 下一页页号 |
file_page_lsn | 8字节 | 页面最后被修改对应的lsn(日志序列号) |
file_page_type | 2字节 | 该页的类型,比如:数据页,溢出页等 |
file_page_file_flush_lsn | 8字节 | 仅在系统表空间中定义 |
file_page_arch_log_no_or_space_id | 4字节 | 该页属于哪个表空间 |
- file_page_offset: 每个页都有单独的页号,innodb通过页号来唯一定位一个页
- file_page_prev、file_page_next: innodb通过这两个字段就把所有的页组成了一条双向链表
File Trailer
InnoDB 操作数据都是以页为单位,加载到内存,已经刷新到磁盘,都以页为单位,当页面在内存中被修改了,需要刷新磁盘的时候,这个时候机器故障停机了,怎么办,我们不知道 当前的页面是否已经刷新完成,或者是只刷新了一半,所以innodb就设计了这个File Trailer, 占用8个字节
- 前4个字节存储校验和: 当需要刷新数据到磁盘的时候,优先算出来校验和然后记录到File Header和File Trailer中,刷新的时候fileHeader就会被优先刷新到磁盘,如果刷新到一半出现了故障, 那么File Trailer中的校验和与File Header中的会不一致,说明刷新的时候出现了故障
- 后4个字节存储页面最后被修改的LSN的后4个字节: 正常情况应该和File Header中的lsn一致,也是用于检验页是否完整。
原文链接: http://herman7z.site