页
的概念,它是InnoDB
管理存储空间的基本单位,一个页的大小一般是16KB
。页的种类有很多,这篇主要说一下索引(INDEX
)页。下面的表格是索引页的储存结构:
名称 | 中文名 | 占用空间大小 | 简单描述 |
File Header |
文件头部 | 38 字节 |
页的一些通用信息 |
Page Header |
页面头部 | 56 字节 |
数据页专有的一些信息 |
Infimum + Supremum |
最小记录和最大记录 | 26 字节 |
两个虚拟的行记录 |
User Records |
用户记录 | 不定 | 实际存储的行记录内容 |
Free Space |
空闲空间 | 不定 | 页中尚未使用的空间 |
Page Directory |
页面目录 | 不定 | 页中的某些记录的相对位置 |
File Trailer |
文件尾部 | 8 字节 |
校验页是否完整 |
在页的7个组成部分中,我们自己存储的记录会按照我们指定的行格式
存储到User Records
部分。
但是在一开始生成页的时候,其实并没有User Records
这个部分,每当我们插入一条记录,都会从Free Space
部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records
部分。
当Free Space
全部用完后,还有新的记录插入则申请新页面,过程如图
记录头信息
名称 | 大小(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 | 表示下一条记录的相对位置 |
插入四条数据 (1, 100, ‘aaaa’), (2, 200, ‘bbbb’), (3, 300, ‘cccc’), (4, 400, ‘dddd’)后的效果图
delete_mask
属性标记着是否删除 0 未删除 1 删除 (对,哪怕真删也还在。)min_rec_mask
B+
树的每层非叶子节点中的最小记录都会添加该标记,0表示并非B+
树非叶子节点中的最小记录 。heap_no
这个属性表示当前记录在本页
中的位置, 0和1给了最小记录和最大记录,每页默认都有。也就是放在 Infimum + Supremum
部分。(0,1最靠前)record_type
当前记录类型: 0
表示普通记录,1
表示B+树非叶节点记录,2
表示最小记录,3
表示最大记录。 ( record_type
为1
的情况,下回分解)next_record
划重点, 表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。
下一条记录
指得并不是插入顺序,而是按照主键值由小到大的下一条记录。而且规定 Infimum记录的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录) ,不论我们怎么对页中的记录做增删改操作,InnoDB始终会维护一条记录的单链表,链表中的各个节点是按照主键值由小到大的顺序连接起来的。
n_owned
属性表示该记录拥有多少条记录,也就是该组内共有几条记录。页
的尾部的地方,这个地方就是所谓的Page Directory
,也就是页目录
。页面目录中的这些地址偏移量被称为槽
(英文名:Slot
),所以这个页面目录就是由槽
组成的。比方说现在的page_demo
表中正常的记录共有6条,InnoDB
会把它们分成两组,第一组中只有一个最小记录,第二组中是剩余的5条记录
对于最小记录所在的分组只能有 1条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。所以分组是按照下边的步骤进行的:
页目录
中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned
值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。页目录
中新增一个槽
来记录这个新增分组中最大的那条记录的偏移量。多插点数据效果如图,省略部分记录之间的箭头和一些属性。
接着二分法查找所需要的记录(比方说我们想找主键值为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
属性遍历该槽所在的组中的各个记录。
仅针对索引页。
名称 | 占用空间大小 | 描述 |
---|---|---|
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
这个状态表示。当然,如果最后一条记录的插入方向改变了的话,这个状态的值会被清零重新统计。
File Header
针对各种类型的页都通用,也就是说不同类型的页都会以File Header
作为第一个组成部分,它描述了一些针对各种页都通用的一些信息,固定为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_OFFSET
:每一个页
都有一个单独的页号,就跟你的身份证号码一样,InnoDB
通过页号来可以唯一定位一个页
。FIL_PAGE_TYPE
:这个代表当前页
的类型。FIL_PAGE_PREV
和FIL_PAGE_NEXT
:分别代表本页的上一个和下一个页的页号,通过建立一个双向链表把许多的页串联起来。并不是所有页都有这个属性的,但是索引页
有。
为了检测一个页是否完整,设计狮在每个页的尾部都加了一个File Trailer
部分,这个部分由8
个字节组成,可以分成2个小部分:
File Header
中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header
在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在File Header
中的校验和就代表着已经修改过的页,而在File Trialer
中的校验和代表着原先的页,二者不同则意味着同步中间出了错。这个File Trailer
与File Header
类似,都是所有类型的页通用的。
数据页
。File Header
,表示页的一些通用信息,占固定的38字节。Page Header
,表示数据页专有的一些信息,占固定的56个字节。Infimum + Supremum
,两个虚拟的伪记录,分别表示页中的最小和最大记录,占固定的26
个字节。User Records
:真实存储我们插入的记录的部分,大小不固定。Free Space
:页中尚未使用的部分,大小不确定。Page Directory
:页中的某些记录相对位置,也就是各个槽在页面中的地址偏移量,大小不固定,插入的记录越多,这个部分占用的空间越多。File Trailer
:用于检验页是否完整的部分,占用固定的8个字节。next_record
属性,从而使页中的所有记录串联成一个单链表
。InnoDB
会为把页中的记录划分为若干个组,每个组的最后一个记录的地址偏移量作为一个槽
,存放在Page Directory
中,所以在一个页中根据主键查找记录是非常快的,分为两步:
File Header
部分都有上一个和下一个页的编号,所以所有的数据页会组成一个双链表
。LSN
值,如果首部和尾部的校验和和LSN
值校验不成功的话,就说明同步过程出现了问题。
来一张全景图: 放大查看