页结构
相信大家之前都听说过innodb的最小数据存储单位就是页,当然innodb当中的页有许多类型。我们今天讨论的是索引页。
| 类型名称 | 十六进制 | 描述 |
|---|---|---|
| FIL_PAGE_TYPE_ALLOCATED | 0x0000 | 最新分配,还未使用 |
| FIL_PAGE_UNDO_LOG | 0x0002 | undo日志页 |
| FIL_PAGE_INODE | 0x0003 | 存储段的信息 |
| FIL PAGE IBUF FREE LIST | 0x0004 | Change Buffer 空闲列表 |
| FIL PAGE IBUF BITMAP | 0x0005 | Change Buffer 的一些属性 |
| FIL PAGE TYPE SYS | 0x0006 | 存储一些系统数据 |
| FIL PAGE TYPE TRX SYS | Ox0007 | 事务系统数据 |
| FIL PAGE TYPE FSP HDR | Ox0008 | 表空间头部信息 |
| FIL PAGE TYPE XDES | 0x0009 | 存储区的一些属性 |
| FIL PAGE TYPE BLOB | 0x000A | 溢出页 |
| FIL PAGE INDEX | Ox45BF | 索引页,也就是我们所说的数据页 |
介绍一下我们今天的主角——————索引页(index_page)
页是Innodb管理存储空间的基本单位,一个页的大小一般是16Kb(当然也是可以在mysql安装初始化的时候进行修改的,初始化之后就不能进行修改了,当然我相信你不会去这么干😂)
索引页结构
让我们来看看这个16kb的空间都包含什么吧👀
首先请不要慌张😫,我们今天并不会按照顺序去理解索引页,从我们好理解的记录存在的位置开始聊起~~
当然我建议你可以先去看一下之前分享过的行格式看起来会更加有感觉
记录在页中的存储
在👆图中的7个组成部分中,我们存储在表中一条记录(数据)会按照我们指定的行格式(默认为dynamic)存在User Records中。但是刚开始生成页的时候,并没有User Records部分,每当我们插入一条记录时,都会先从Free Space中申请出来一个记录(数据)大小的空间,并将这个空间划分到User Records中。
好,stop~。现在我们回忆一下行格式
一切都要从行记录中的记录头开始说起~
首先来建个表
CREATE TABLE page_demo(
c1 INT,
c2 INT,
c3 VARCHAR(10000),
PRIMARY KEY (c1)
) CHARSET=ascii ROW_FORMAT=COMPACT;
在上述表中,我们指定了主键,所以innodb不会给我们创建隐藏列ROW_ID
| 名称 | 大小(bit) | 描述 |
|---|---|---|
| 预留位1 | 1 | 没有使用 |
| 预留位2 | 1 | 没有使用 |
| deleted_flag | 1 | 标记该记录是否被删除 |
| min_rec_flag | 1 | B+Tree中每层非叶子节点中的最小的目录项记录 |
| n_owned | 4 | 页中的记录会进行分组(后面会讲),没有组中的大哥会在这里记录组里一共有多少组员。组员该标记为0 |
| heap_no | 13 | 表示该记录在页面堆中的相对位置 |
| record_type | 3 | 表示该记录的类型,0为普通记录,1为B+Tree非叶子节点的目录项记录,2表示Infimum记录,3表示Supremum记录 |
| next_record | 16 | 表示下一条记录的相对位置 |
| 为了便于理解,我们只列出与上面page_demo表相关的内容 |
这里算是重温了前面讨论过的行格式相关内容,下面我们插入一些数据
INSERT INTO page_demo VALUES(1,100,'aaaa'),(2,200,'bbbb'),(3,300,'cccc'),(4,400,'dddd');
为了便于理解,我们将记录头信息使用10进制表示(实际是二进制),注意图中各记录间有空隙,但是实际上没有没有的
- deleted_flag:这个属性使用来标记当前记录是否被删除了,0为没有,1为被删除。Waht?我都删除了记录,它还存储在磁盘嘛?是的,这个是innodb的老头们考虑到如果直接从磁盘移除,会导致需要对其他的记录进行重新排列,这样会带来性能上的损耗。所有删除的记录会组成一个垃圾链表,称为“可重用空间”。之后如果有新记录插入到表中,它们可能就覆盖被删除的记录占用的存储空间。
- 标记deleted_flag为1和被删除的记录加入垃圾链表其实是2个阶段,我们在后面介绍undo日志的时候再细说
- min_rec_flag:B+Tree上非叶子节点的最小目录项记录会添加该记录。如果你还不知道上述名词,不着急,后面说
- n_owned:这个马上就细说,现在先不带入概念
- heap_no:我们存放在User Recordsd当中的记录是连续是,这片连续的结构称为堆(heap),所以为了方便管理,大叔们将一条记录(deleted_flag可为1)在堆中的相对位置称之为heap_no。位于页前面的记录heap_no较小,位于后面的较大。讲人话:就是为每一条记录进行位置记录,步进长度为1。好奇👶该说了:我怎么看上面4条记录是从2开始的?没毛病!老铁,因为这是设计innodb的👴们故意这样做的,他们会自动给页加上2条记录,我们称它“伪记录”。一条是代表页中的最小记录(Infimum记录),一条便是代表页中最大的记录(Supermum记录);这里也体现了页中的记录是可以比较大小的,不然👴们怎么会这样干呢~ 从图中,可以看出记录是通过主键升序排列的。
- 注意点:因为Infimum和Supermum2个家伙是🧍♂️们默认创建的记录,所以它们是不存放在User Records部分,而是单独放在一个名为Infimum+Supremum的部分
- 注意点:heap_no值只要分配了就不会改变,即使记录被删除
- record_type:这个属性就是表示当前页的类型。一种4种类型,0为普通记录,1为B+Tree非叶子节点的目录项类型。2为Infimum记录,3表示Supermum记录。0、2、3我们都知道了,1留在后面索引再细说
- next_record:这个属性real重要,它表示当前记录的真实数据到下一条记录的真实数据的距离。如果是正数,代表下一条记录在当前记录的后面,好奇👶又发现问题了:记录4怎么是负数?不是一直单链表下去的嘛?没毛病,老铁!别忘了我们的Supermum记录,它是代表最大的记录,所以记录4要往前指向Supermum!
- 当我们删除其中一条记录,单向链表也会随之改变。但是删除的记录并不会从存储空间中移除,而是该条记录的deleted_flag会设置为1、next_record值会变为0、值得注意的是Supremum记录的n_owned值从5变成了4!
Page Directory(页目录)
现在我们知道了索引页中的记录通过单向链表进行连接存储,那么我们怎么去查找数据呢?
SELECT * FROM page_demo WHERE c1 = 3;
我们可以从Infimum开始一直找下去,不就找到了~ 。这样也可以,但是如果数据体量增多,这种方式无疑是致命的!这里就不得不说了设计innodb的🧍♂️的智慧了作为为一名优雅的🧑💻,肯定要使用更加优雅的实现啦!首先我们现在将一本书籍比做innodb中的页,当我们要找某个内容的时候,是不是先从目录得到对应的页码,然后到对应页码中去寻找!而🧍♂️们就是用的这种思想:
- 将页中所有的记录(包括infimum和Supremum记录,但不包含已经移除到垃圾链表的记录)划分为几组(理解成书籍中的章节)
- 每组的最后一条记录(就是组内最大的那条记录)的头信息中的n_owned属性表示该组一共有几条记录
- 将每组中最后一条记录在页面中的地址偏移量(就是该记录的真实数据与页面中的第0个字节之间的距离)单独提取出来,按顺序存储到靠近页尾部的地方。这个地方就是我们的Page Directory。页目录中的这些地址偏移量称为槽(Slot),每个槽占用2字节。页目录由多个槽组成的。
- 分组规则
- 在初始化情况下,一个数据页只有Infimum记录和Supermum记录这两条,它们分属于两个分组。页目录中也只有两个槽,分别代表Infimum记录和Supermum记录在页面中的地址偏移量
- Infimum记录所在分组只能由1条记录(本身),Supremum记录所在的分组拥有的记录条数只能在1~8条之间,其他的分组记录条数范围只能在4~8条之间
- 到这里,我们思考一下设计innodb的大叔们,为啥要这样设计出一个页目录呢?其实就是为了解决上述提出的如何进行快速操作数据的问题。我们由上面的知识知道在页中的数据都会按照主键的大小进行升序进行分组排列,而槽记录是组内主键最大的记录。既然数据是排好序的,那么我们是不是可以使用经典二分查找的方式进行快速寻找数据!
Page Header(页面头部)
页面头部主要是存储一些关于该索引页的记录的状态信息,比如当前索引页已经存储了多少条记录、Free Space在页面中的地址偏移量、页目录中存储了多少个槽等,它属于页结构的第二部分,占用固定的56个byte。下面是各个字节的具体记录值。
| 状态名称 | 占用空间大小 | 描述 |
|---|---|---|
| PAGE_N_DIR_SLOTS | 2byte | 在页目录中的槽数量 |
| PAGE_HEAP_TOP | 2byte | 还未使用的空间最小地址,也就是说该地址后面就是Free Space |
| PAGE_N_HEAP | 2byte | 第一位表示本记录是否为紧凑型的记录,剩余的15位表示本页的堆中记录的数量(包括Infimum和Supremum记录以及标记为‘已删除’的记录) |
| PAGE_FREE | 2byte | 各个已删除的记录通过next_record组成一个单向链表,这个单向链表中的记录所占用的存储空间可以重新利用:PAGE_FREE表示该链表头节点对应记录在页面中的偏移量 |
| PAGE_GARBAGE | 2byte | 已删除记录占用的字节数 |
| PAGE_LAST_INSERT | 2byte | 最后插入记录的位置 |
| PAGE_DIRECTION | 2byte | 记录插入的方向 |
| PAGE_N_DIRECTION | 2byte | 一个方向连续插入的记录数量 |
| PAGE_N_RECS | 2byte | 该页中用户记录的数量(不包含Infimum和Supremum记录以及被删除的记录) |
| PAGE_MAX_TRX_ID | 8byte | 修改当前页的最大事务ID,该值仅在二级索引页面中定义 |
| PAGE_LEVEL | 2byte | 当前页在B+Tree中所在层级 |
| PAGE_BTR_SEG_LEAF | 10byte | B+Tree叶子节点的头部信息,仅在B+Tree的根页面中定义 |
| PAGE_INDEX_ID | 8byte | 索引ID,表示当前页数语哪个索引 |
| PAGE_BTR_SEG_TOP | 10byte | B+Tree非叶子节点段的头部信息,仅在B+Tree的根页面中定义 |
别慌各位,是不是头有点痒,你先别痒!上面的状态信息看不明白,是因为我们还没有学习后面的内容而已!但是现在你已经知道的是PAGE_N_DIR_SLOTS、PAGE_LAST_INSERT以及PAGE_N_RECS的意思;
- PAGE_DIRECTION:这个是用来记录新插入记录的方向的,如果新插入的记录的主键比上一条记录主键大,那么插入方向就是右边,反之就是左边;
- PAGE_N_DIRECTION:如果连续插入的新记录的方向都是一致的,Innodb会把沿着同一方向插入记录的条数记录下来,讲人话就是一个同方向计数器!但是如果新记录方向不一致,就会清零重新统计。
File Header(文件头部)
到这里各位可以松口气了,后续的内容都是小菜。前面讨论的Page Header是专门记录索引页的各种状态信息,现在说的File Header是通用于所有类型的页的,它描述了一些通用于各种页的信息,固定占用38字节;
| 状态名称 | 占用空间大小 | 描述 |
|---|---|---|
| FIL_PAGE_SPACE_OR_CHKSUM | 4字节 | 4.0.14在之后的版本 |
| 中,该属性表示页的校验和(checksum) | ||
| FIL_PAGE_OFFSET | 4字节 | 页号 |
| FIL_PAGE_PREV | 4字节 | 下一个页的页号 |
| FIL_PAGE_LSN | 8字节 | 页面被最后修改时对应的ISN ( Log Sequence |
| Number,日志序列号)值 | ||
| FIL_PAGE_TYPE | 2字节 | 该页的类型 |
| FIL_PAGE_FILE_FLUSH_LSN | 8字节 | 仅在系统表空间的第一个页中定义,代表 |
| 文件至少被刷新到了对应的 LSN 值 | ||
| FITL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4字节 | 页属于哪个表空间 |
别慌别慌,现在我们只看我们能看懂的。
- FIL_PAGE_SPACE_OR_CHKSUM:这个就是代表当前页面的校验和。就是对于一个很长的字节串来说,我们通过算法来计算出一个比较短的值来代表这个长字节串。那么在比较这2个长字节串的时候,先比较校验和。如果校验和都不对,那肯定是不一样的。节约时间。
- FIL_PAGE_TYPE:表示当前页的类型。一开始我们就提及了各种类型的页
- FIL_PAGE_PREV和FIL_PAGE_NEXT:因为我们的表一般数据都会比较多,Innodb无法一次性为这么多数据分配一个巨大的存储空间,所以就使用双向链表的结构来进行关联起来,而无须这些页在物理上真正连着(不是所有类型的页都有这个属性!!!索引页是有的) 还是那句话,其他的属性,用到再提~
File Trailer(文件尾部)
我们知道,InnoDB 存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以页为单位把数 据加载到内存中处理。如果该页中的数据在内存中被修改了,那么在修改后的某个时间还需要把数 据刷新到磁盘中。但是,如果在刷新还没有结束的时候断电了该咋办,这不是相当尴尬么?为了 检测一个页是否完整(也就是在刷新时有没有发生只刷新了一部分的尴尬情况),设计 InnoDB 的 大叔在每个页的尾部都加了一个 File Trailer 部分,这个部分由8字节组成,可以分成2个小部分。 前
- 4字节代表页的校验和。这个部分与 File Header 中的校验和相对应。每当一个页面 在内存中发生修改时,在刷新之前就要把页面的校验和算出来。因为 File Header 在页 面的前边,所以 File Header 中的校验和会被首先刷新到磁盘,当完全写完后,校验和 也会被写到页的尾部。如果页面刷新成功,则页首和页尾的校验和应该是一致的。如 果刷新了一部分后断电了,那么 File Header 中的校验和就代表着己经修改过的页,而 File Trailer 中的校验和代表着原先的页,二者不同则意味着刷新期间发生了错误。
- 后4字节代表页面被最后修改时对应的LSN 的后 4字节,正常情况下应该与 File Header 部分的 FIL PAGE LSN 的后4字节相同。这个部分也是用于校验页的完整性, 不过我们目前还没说LSN 是什么意思,所以大家可以先不用管这个属性。