2、InnoDB数据页
2021-10-14 16:02:45 1 举报
InnoDB数据页结构,剖析InnoDB数据页原理实现
作者其他创作
大纲/内容
1
next_record: 表示当前记录的真实数据到下一条记录真实数据的地址偏移量。比如说第一条记录的next_record值是30,就表示从当前位置的地址向后找30个字节就是下一条记录的真实数据,这其实就是一个链表 但需要注意的是,下一条记录并不是指我们插入顺序的下一条记录,而是按照主键从小到大排序之后的下一条记录。而且规定最小记录的下一条记录是本页中主键最小的用户记录,本页主键最大的用户记录下一条就是最大记录。
到这里基本上头信息的部分肝的也差不多了,但还是差一个就是n_owned,咱们接着往下瞧!
Free Space(不确定)
Page Header
Page Directory
233
0
页类型表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 索引页,也就是我们所说的数据页
b
heap_no
Infimum+supremum(26字节)
Infimum+supremum
File Header
data...
a
nect_record
Page Header(56字节)
5
delete_mask
Free Trailer(8字节)
结论:在页的7个组成部分中,我们自己存储的记录会按照我们指定的行格式存储到User Records中。但是在一开始生成页的时候,其实并没有User Records这个部分,每当我们插入一条记录,都会从Free Space部分,也就是在尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页已经使用完了,如果还有新记录插入的话,就需要去申请新的页了!
为了更好地管理User Records,还是要通过InnoDB行格式中的记录头信息中说起
User Records
无论数据以什么样的形式展示,都是要存储一些关于它自己本身的信息,比如先前的行格式中的记录头信息,那么现在存储行数据的页也拥有它自己的头信息,我们称之为Page Header(页头部信息),这部分固定真有56个字节,具体如下:page_n_dir_slots 2字节 在页目录中的槽数量page_heap_top 2字节 还未使用的空间最小地址,也就是说从该地址之后就是Free Spacepage_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_n_directionpage_direction: 假设现在有一条新记录插入,这条新记录的主键比上一条记录的主键值大,就可以说这条记录的插入方向是右边,反之就是左边。page_n_direction: 假设连续好几次新记录插入的方向都是一致的,那么就会沿着这个方向记录下来插入记录的条数。当方向改变时清零重新统计
最大记录
3
接下来就是File Header(文件头部)这个File Header是针对各种类型的页的是通用的,不同类型的页都会有File Header这部分组成,它描述了各种页的一些通用信息。这部分固定占38个字节。fil_page_space_or_chksum 4字节 页的校验和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值file_page_arch_log_no_or_space_id 4字节 页属于哪个表空间fil_page_space_or_chksum: 表示当前页的校验和。就是对于一个非常畅的字节串来说,通过某种算法计算出一个比较短的值,这个比较短的值就是校验和。然后在比较的时候先比较 校验和,如果校验和都不一样,哪个长字节串肯定不一样,这就省去了直接比较两个比较长的字节串的时间损耗 fil_page_offset : 每一个页都有它单独的页号,是唯一的,InnoDB可以通过页号来定位一个页fil_page_type: 表示当前页的类型,可以参考旁边的页类型表fil_page_prev和fil_page_next: 在InnoDB中数据以页为单位存储数据的,所以在数据很大的时候InnoDB就会拆分出好多个页来,而这两个属性就是存储上下两页的位置。在逻辑上相互连着,物理上无需真正连着。熟悉数据结构一定能看出来这就是一个双向链表
记录在数据页中存储过程
1、当next_record的值为0时,意味着该记录已经没有下一条记录了。2、如果说其中的某一条记录的delete_mask标记为删除,那么它的上一条记录的地址就会指向它下一条记录的地址。如果再插入相同数据的时候,由于存储空间没有被回收,将直接恢复地址指向。你会不会觉得next_record这个指针有点儿怪,为啥要指向记录头信息和真实数据之间的位置呢?为啥不干脆指向整条记录的开头位置,也就是记录的额外信息开头的位置呢?因为这个位置刚刚好,向左读取就是记录头信息,向右读取就是真实数据。我们前边还说过变长字段长度列表、NULL值列表中的信息都是逆序存放,这样可以使记录中位置靠前的字段和它们对应的字段长度信息在内存中的距离更近,可能会提高高速缓存的命中率。
4
delete_mask: 标记当前记录是否被删除,占用1个进制位,当值为0是表示没有被删除,为1是表示当前记录被删除掉了。这里有一个问题,难道InnoDB中被删除的记录还存在页中的吗? 是的,没错。这么做似乎像是我们平时工作中的逻辑删除。但其实还是存在一定的差别的。当当前记录被标记为删除之后,当前记录还存在磁盘当中,并不会立即从磁盘移除掉。这里主要考虑如果每一次删除操作都要去操作磁盘真实删掉数据重新排列的话,是非常消耗性能的。所以,这里只是先打上一个删除标记,被删除的记录了都会组成一个垃圾链表,在这个链表当中的记录占用的空间称之为可重用空间,如果之后有新记录插入的话,可能会把这些被删除记录的存储空间覆盖掉。将这个delete_mask位设置为1和将被删除的记录加入到垃圾链表中其实是两个阶段,在后面事务哪里会有详细介绍。
Free Space
Free Trailer
User Records(不确定)
Page Directory(页目录): 在一个页中的数据都是由主键从小到大排序形成的一个单链表,那如果我们需要从这个页中查找某条记录该怎么办?难道还要从最小记录开始遍历去查询吗?如果数据少还好,但如果数据一旦上来,这样查询的速度那简直太慢了。所以这个笨办法在InnoDB中当然是不会出现的了。 设计InnoDB的大佬采用了一种更6的办法,就是使用页目录。它类似于我们看到书的目录。整个目录的生成过程是这样的: 1、将所有正常的记录(包括最小&最大记录,不包括标记为删除的记录)划分为几个组 2、每个组的最后一条记录(也就是组内最大的记录)的头信息中的o_owned属性表示该组拥有的记录数,也就是该组内有几条记录 3、将每组最后一条记录的地址偏移量提取出来按顺序存储到Page Directory(页目录)中。将存储在页目录中的地址偏移量称之为槽(slot)
File Header(38字节)文件头部 (页的一些通用信息)Page Header(56字节)页头部(数据页专有的一些信息)Infimum+supremum(26字节)最小纪录和最大记录(两个虚拟的行记录)User Records(不确定)用户记录(实际存储的行记录内容)Free Space(不确定)空闲空间(页中尚未使用的空间)Page Directory(不确定)页目录(页中的某些记录的相对位置)Free Trailer(8字节)文件尾部(校验页是否完整)
30
最后就是File Trailer(文件尾部)部分,它由两部分组成,一共需要8个字节前四个字节表示页的校验和: 这个与File Header中的校验和是对应的。如果一个页在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header在页的前面,所以校验和会先被同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,两个校验和应该是一致的。如果同步到一半断电了,那么在File Header中的校验和就代表着已经修改过的页,而在File Trailer中的校验和代表着原先的页,二者不同则意味着同步中间处了问题后四个字节表示页面被最后修改时对应的日志序列位置
109
data
min_rec_mask
1、在页目录当中存储着两个槽,也就是当前页里面的记录被分为了两个组,槽0的值是21,表示最小记录的偏移量,槽1的值是109,表示最大记录的偏移量2、这里需要注意每组最大记录中o_owned属性的值 槽0记录对应的值中o_owned为1,表示当前组中只有1条记录,也就是它本身 槽1记录对应的值中o_owned为5,表示当前组中有5条记录 每个分组中的记录条数都是有规定的: 对于最小纪录所在的分组只能有1条记录 最大记录所在的分组拥有的记录数只能在1~8条之间 剩下的分组中记录的条数范围只能在4~8条之间 所以分组的步骤是这样进行的: 1、初始情况下对数据页里只有最小纪录和最大记录,他们分别属于两个分组 2、之后每插入一条数据,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的o_owned值加1,表示本组内添加了一条记录,直到该组中的记录数等于8个 3、在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程页目录中新增一个槽来记录这个新增分组中最大记录的偏移量
2
经过这一系列的操作,最后在页目录中存储了很多的槽,假设这时候我们需要按照主键去查询一条数据,由于在页目录中的槽已经按照主键进行了排序,可以使用二分法的方式查询某一个槽,找到这个槽之后我们就可以按照之间所说的笨方法遍历这个槽组中的数据了。毕竟这时候组内的数据最多也才才8条数据,遍历起来也不耗费性能。
Page Directory(不确定)
槽0
67
heap_no: 表示当前记录在本页中的位置。从上面的一条记录中看出来,这条记录的位置是2。似乎少了点啥,0和1呢? 其实在每个页里面都自动加了两个记录,由于这两条记录并不是我们自己插入的,所有也称之为伪记录或者虚拟记录,这两条记录中一个代表最小记录,一个代表最大记录。 唉?这句话似乎在说记录可以比大小?是的没错,记录是可以比大小的,比较记录的大小其实就是比较主键的大小。 这两条记录被放在页中的Infimum + Supremum部分。
min_rec_mask: B+树的每层非叶子节点中的最小纪录都会添加该标记,目前这条记录插入的值是0,表示不是B+树非叶子节点中的最小纪录。
42
由于读取磁盘的IO非常之慢,mysql采用数据页的方式进行内存与磁盘之间的交互,默认情况下一页是16KB。一次性从磁盘中最少拿16KB数据到内存中;一次性最少将16KB数据从内存刷到磁盘中当然,页也被分为了好多种类型,比如存放表空间头部信息的页、insert buffer信息的页等等。这里并不阐述这些,而是重点说一下存放真实数据的页,官方称之为 index页(索引页)。我们姑且称之为数据页吧
File Header(38字节)
56
21
上图当中被分为了两个组,其实看o_owned就能看出来,这里将Page Directory加上
页16KB
最小记录
n_owned
槽1
record_type
record_type: 表示当前记录的类型,一共有四种类型,0:普通记录 1:表示B+树非叶子节点记录 2:最小记录 3:最大记录
0 条评论
下一页