数据页结构的大概
首先我们先来了解一下,InnoDB的存储单元是数据页的概念,页的大小一般是16KB,而InnoDB里面存放了很多不同目的 的数据页,比如存放Insert Buffer的信息页,Undo的日志页等等。 但是这里我们主要讲解的是存放表中的数据页,官方称这种存放数据的页,叫索引页。 接下来我们先大致来了解数据页的结构,总共分为 7个 部分,根据顺序分别为:
File Header:文件头部,占用空间大小为38字节,主要存放页的一些通用信息 Page Header:页面头部,占用空间大小为56字节,主要是数据页专有的一些信息 Infimum + Supermum:最小记录和最大记录,占用空间36个字节,主要是存放两个虚拟的行记录 User Records:用户记录数据,占用空间大小是未知,主要是用来存放记录数据的 Free Space:空闲空间,占用空间大小是未知的,主要是页中没有使用的空间 Page Directory:页面目录,占用空间大小是未知,主要是页中的某些记录的相对位置 File Tailer:文件尾部,占用空间大小为8字节,主要是校验页是否完整 了解了数据页的大概结构,接下来我们就需要了解更加详细的信息,从头到尾接着说
File Header 文件头部
想象一下,当你翻书的时候,特别是对应的百科全书,你想要找到某一页的内容,是否需要一些信息才能更好的找到,比如 页的号码,页的归类等等。 InnoDB 引擎的数据页也需要这些东西,这样才能更快更好的找到所需要的数据,但是 文件头部的信息存储还没有那么详细,他主要是存储一些页的状态信息,因为这样存储可以通用于不同类型的数据页。 文件头部主要存储的状态信息有这些:
FIL_PAGE_SPACE_OR_CHKSUM:页的校验和 FIL_PAGE_OFFSET:页的号码,每个页都有单独的页号 FIL_PAGE_PREV:上一页,之所以存储上一页和下一页是因为数据页不一定是按照顺序来进行分配的,也有可能在存储的各个地方,所以就需要双向索引来进行关联。 FIL_PAGE_NEXT:下一页 FIL_PAGE_LSN:页面被最后修改时对应的日志序列位置 FIL_PAGE_TYPE:这个代表当前页的类型 FIL_PAGE_FILE_FLUSH_LSN:仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值 FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID:页属于哪个表空间
Page Information 页的相关信息
介绍完 文件头部 以后,我们可以来看看页的头部,上面讲了,页的头部更多存储了页的相关信息,主要是为了查询记录更加方便,所以接下来我们就来探索。 由于页的头部信息主要是为了记录做铺垫,所以我们就从记录开始讲解,从里到外一步步来
User Records
首先先创建一个表,为了简便理解,先建一个字符集是ascii,一个字符一个字节,且行格式是Compact的表
CREATE TABLE page_test ( p1 INT , p2 INT , p3 VARCHAR ( 10000 ) , PRIMARY KEY ( p1 ) ) CHARSET = ascii ROW_FORMAT = Compact;
创建完之后,我们来看看表的记录行结构 这个结构,有些上一节已经说过了,有一些没有说,但是这节最重要的是记录的头部信息,头部信息各个指标是:
预留位1:没有使用 预留位2:没有使用 delete_mask:这条记录是否给删除 min_rec_mask:B+树的每层非叶子节点中的最小记录都会添加该标记 n_owned:表示截止到当前记录为止,数据页拥有的记录数 heap_no:表示当前记录在记录堆中的位置信息 record_type:表示当前记录的类型
0 普通记录 1 B+树非叶节点记录 2 最小记录 3 最大记录 next_record:表示下一条记录的位置信息 了解记录头部信息以后,我们再插入几条记录更加直观的看看
INSERT INTO page_test VALUES ( 1 , 1 , '1111' ) , ( 2 , 22 , '2222' ) , ( 3 , 333 , '3333' ) , ( 4 , 4444 , '4444' ) ;
delete_mask:
首先先来看看delete_mask这个字段,为什么会出现这个字段? 因为删除记录的时候,我们不会立即删除,会先给这条记录打上标记,并且将上一条记录的next_record的字段附上我们的next_record字段,并且将我们的next_record字段置为0。并且之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。之所以这么做主要是有两个原因:
移除它们之后把其他的记录在磁盘上重新排列需要性能消耗 如果删除之后这些空间不用,就会有很多碎片化的空间 min_rec_mask:为索引做准备的 heap_no:
这个属性表示当前记录在本页的位置,但是可以从图中看出来,我们是从2开始的,0和1都不在,为什么呢?
这就是InnoDB的一个精巧的设计,在每个页创建之时,就会自动添加进去两条记录,分别是 Infimum 和Supermum ,没错就是最小记录和最大记录,最小记录的heap_no为0 放在最前面,最大记录的heap_no为1放到最后面,而这两条记录的构造都是定死的,都是由 5个字节大小的记录头信息和8个字节大小的固定部分组成的 并且由于 Infimum + Supermum 跟 用户记录是不一样,所以放在 User Records 之前 当作一部分 next_record:它表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。从结构上来看,这记录的存储像一个链表,通过一个记录找到他的下一个链表。还需要注意的是,链表的顺序是按照主键的顺序进行排列的,也就是说一个记录的下一个记录的地址一定是跟他的主键顺序有关的,而且规定 Infimum记录(也就是最小记录) 的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录) 。至于删除之后的操作,在delete_mask已经说过了 n_owned:这个参数十分重要,但是讲解这个需要跟Page Directory 页目录有关,下面我们就跟着页目录一起看看
Page Directory
以上我们了解了记录在页中是按照主键值由小到大顺序串联成一个单链表。但是如果我们想根据主键值查找页中的某条记录,怎么查询呢?是一条一条遍历吗?那效率也太低了,这个时候我们就需要页目录了 就像之前讲的我们在书中找到对应的模块,一般是根据目录的页码去查找,所以这边也会制作一个类似功能的页目录,制作流程如下:
将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录。 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory,也就是页目录(此时应该返回头看看页面各个部分的图)。页面目录中的这些地址偏移量被称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。 备注:对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在1 - 8条之间,剩下的分组中记录的条数范围只能在是 4-8 条之间。所以分组是按照下面的步骤进行的:
初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。 之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。 在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。 下面通过一个例子来讲解 : 假设我们一下子添加12条记录,就会产生下面如图的分组 有了这个槽以后,我们查找对应主键的记录,就方便了多了,主要步骤是:
通过二分法确定该记录的槽,并找到槽中主键值最小的记录 通过记录的next_records 挨个遍历,找到记录
Page Header
根据上面的信息,我们终于可以说页面头部存那些信息了,想必你们都知道了,如果想要更快找到对应的记录,就需要在头部存储一些信息,比如目录存储了多少个槽,第一条记录的地址等等 下面就是对应信息:
PAGE_N_DIR_SLOTS:在页目录中的槽数量 PAGE_HEAP_TOP:还未使用的空间最小地址,也就是说从该地址之后就是Free Space PAGE_N_HEAP:本页中的记录的数量 PAGE_FREE:第一个已经标记为删除的记录地址 PAGE_GARBAGE:已删除记录占用的字节数 PAGE_LAST_INSERT:最后插入记录的位置 PAGE_DIRECTION:记录插入的方向 PAGE_N_DIRECTION:一个方向连续插入的记录数量 PAGE_N_RECS:该页中记录的数量 PAGE_MAX_TRX_ID:修改当前页的最大事务ID,该值仅在二级索引中定义 PAGE_LEVEL:当前页在B+树中所处的层级 PAGE_INDEX_ID:索引ID,表示当前页属于哪个索引 PAGE_BTR_SEG_LEAF:B+树叶子段的头部信息,仅在B+树的Root页定义 PAGE_BTR_SEG_TOP:B+树非叶子段的头部信息,仅在B+树的Root页定义
File Trailer
由于InnoDB 存储引擎会把数据存储到磁盘上,但是磁盘速度太慢了,需要以页为单位把数据加载到内存中处理,如果有修改,则在某个时间点同步到磁盘中,但是如果在同步中出现一些不可抗力因素失败了,则需要一些信息来判定数据是否完整 前4个字节代表 页的校验和:
这个部分是和File Header中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header在页面的前面,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在File Header中的校验和就代表着已经修改过的页,而在File Trialer中的校验和代表着原先的页,二者不同则意味着同步中间出了错。 后4个字节代表页面被最后修改时对应的日志序列位置(LSN):
这个部分也是为了校验页的完整性的,只不过我们目前还没说LSN是个什么意思,所以大家可以先不用管这个属性。