mysql指引(八):innodb页结构
前言
上篇文章 中说到了Innodb页内的数据行结构,实际上什么都不需要记住,只需要知道数据行中有记录头,而记录头中有关键的信息,这些信息会和后续学习事务、锁等特性相关。
本篇,我们就来看下Innodb的页结构,实际上,前两篇文章中也或多或少的提到了页结构相关的内容。
页结构的意思就是:innodb以页这个逻辑概念为单位,将存储的数据和其他信息划分到一个个的页中。我们需要了解的就是这些页中的数据(泛指)是如何组织的。
页结构
页中可分为三类结构,即三块区域:
- 需要存储数据的结构
- 加快访问的数据结构
- 页自身的信息结构
而这三类结构就包含了下面七个部分,这七个部分,就是innodb页结构的划分,即页被划分成了下面7块:
- File Header
- Page Header
- Infimum 和 Supremum
- User Records
- Free Space
- Page Directory
- File Trailer
我们按照三种结构划分,来看看这七项内容。
需要存储数据的结构
假如是图书馆书架上的一排放书的空间,随着购入的书增多,这个空间会逐步被塞满。随着有些书被借出去,对应位置就会空出来,当这些书再还回来时又会被塞入相应的位置。
上面的描述,实际上展示了三条链表:
- 有书链表,即一本书的旁边是另外一本书,一个接着一个
- 空闲链表,即还可以放入新购入图书的空间
- 借走链表,即这些位置是被借走的书,后面还会再回来的。即可被重复使用。
对应到数据库这边,则是 User Record
和 Free Space
:
- 初始状态没有数据,即书架没书。实际上这段空间就是 Free Space
- 后来加入了数据,即书架新购入图书,则相当于占据了 Free Space 的一部分,这部分就是 User Record。前文所讲的数据行就构成了 User Record。
- 当某些数据被删除后,因为空间可以重复利用,所以程序内部会维护一个垃圾链表,来达到目的。
加快访问的数据结构
页之间的访问由B+树来实现快速访问,从而避免了大量链表的遍历,那么页的数据行之间的访问如何实现加速呢?假如有1万个数据行,难道要从头开始遍历下去吗?虽然数据页是被加载到内存中的,但是遍历这么多还是有点浪费的,能否加速呢?
Page Directory
和 Infimum 和 Supremum
就是用来实现加速访问效果的结构。数据行之间是通过链表串起来的,那么 Infimum就相当于链表的头结点,Supremum就相当于链表的尾部结点。
所以 Infimum 的下一条记录就是第一个用户数据行数据,即 User Record 里面的第一个数据行;而 User Record 里面的最后一个数据行,其下一个记录就是 Supremum。
那么,这两个节点有什么意义呢?看起来好像没多大作用。是的,单独来看,确实没什么用处。但是和 Page Directory 结合起来则威力无穷。
通过前文的学习,我们知道下面两个知识点:
- 每个索引都是一棵B+树
- 每个页内的数据行是按照索引大小顺序来排序递增下去的
假如主键id的值为1~100,这一百个数全部存在一个数据页中。则有100个数据行,按照id=1 开始一直往下排到 id = 100.
我们要查询 id = 37 的数据,如果避免遍历前面36个节点呢?
innodb采用的方法是:
- 将这100个数据行进行分组,比如每10个为一组,然后将每组中索引值最大的数据行地址(偏移)记录下来。那么我们可以记录下,即如下id值对应的偏移:
10,20,30,40,50,60,70,80,90,100…
这些记录(id值和偏移)就放到 Page Directory 区域中。
- 当用户查找时,比如 id = 37,则首先去 Page Diretory中查找,利用的算法是二分法,首先找中间的数值,比如找到了 id = 50,比较发现 37 < 50,又因为数据行是按照顺序存放的,所以继续在
10,20,30,40,50
中寻找,最终可以找到 id = 30 的偏移。此时拿着偏移量再去 User Record 里面找对应的数据行,即直接找到了 id = 30的数据,30 < 37,继续向后遍历即可。
数据行越多,这样的处理方法带来的收益越大。
- 实际上,在数据插入的时候,innodb就在 Page Directory 中构造这些id了,这些也叫做
slot
即槽
。
因为最开始 User Record 中没有任何记录,所以 Page Directory 中也没有任何的 slot。如果此时插入一条数据,则 slot 该如何划分?所以在这种初始化的状态,Infimum 和 Supremum 就派上了用场。配合事先定义的划分规则,可以方便的去逐步构造出 Page Directory 中的各个槽,具体规则就不说了。知道 Page Directory 的作用即可。
页自身的信息结构
页自身的信息结构包含三个部分:
- File Header
- Page Header
- File Trailer
首先来说 File Trailer
,就是用来校验 页 中数据有没有损坏的,这个是可能出现的。比如从磁盘中读取数据时,突然断电等。所以必须要有校验,否则基于错误的数据源,后果不堪设想。
再然后是两个 Header,这其实和 数据行结构有点类似的感觉,数据行结构中也有 Header,是记录头信息,记录了数据行内部的一些信息,以及数据行与数据行之间的信息。
同理,这两个Header也记录了这两个方面的信息,我们就来分别看一下。
首先是 File Header
,因为页有很多种类型,数据页只是其中之一。所以 File Header 中是各个页面通用的属性,比如上一页下一页的地址,该页是什么类型。其他的等遇到了再说,只要知道这个结构即可。
小结下:FIL_PAGE_PREV 字段就是上一页的地址,FIL_PAGE_NEXT 就是下一页的地址,FIL_PAGE_TYPE 就是页的类型。当然还有其他的,先不考虑。
其次,是 Page Header
,专门用来表示数据页的基本信息的。比如 slot 的数量等。
因为每个索引都是一棵B+树,而且可能会有很多索引,所以 Page Header 中还记录了这个页属于哪个索引;又因为每棵B+树的叶子节点(即最底层节点)才是存储数据的,其他层的节点都是用来快速定位到叶子节点的,所以还需要记录该页是否是叶子节点,或者说该页处于 B+树 中的哪一层次。
小结下:
PAGE_N_DIR_SLOTS 就是 slot 的数量,PAGE_LEVEL 就是该页在 B+树中的层次,PAGE_INDEX_ID 就是该页对应哪个索引。其他的,遇到了再记即可。
补充
页结构说完了,配合前两篇文章,这块应该是比较清晰了。现再补充几点:
- 我们提到过表空间,即 System tablespace 和 用户表的.ibd文件对应的表空间。
上文说到的 Page Header 中的 PAGE_INDEX_ID 只是一个数值,比如等于10,即该页对应索引ID为10的索引,那这个索引到底属于哪个字段呢?
还有前文提到的,数据行结构中并没有存储定长字段的长度,那么这些长度去哪里看呢?等等一些信息,实际上都是去表空间中看的,系统表空间中有一块存储的是Data Dictionary
即数据字典,这里面就放了这些信息。后面我们还会再次提到的。
- 一个数据表对应一个 .ibd 文件,该文件包含了这些页数据。
前文也整理了文件、页和真实数据的联系图。比如表空间大小为 1GB,页16KB,则总共有65536个页。所以 File Header 中还有一个字段FIL_PAGE_OFFSET
表示该页在表空间中的偏移值。先这样提一下。
- 每棵B+树都有一个根节点,实际上创建索引的时候,首先创建的就是根节点,然后随着数据的填充,才逐步变成了多层的B+树。
那么根节点一旦创建出来,其页号就不会变了。当innodb第一次查找数据的时候,通过根节点当然可以找到其他节点,但是他怎么知道根节点在哪里呢?
根节点也存在 Data Dictionary
中,所以表空间还是比较重要的。
好了,关于页及页内数据行这个层级的结构描述完了,再回到这张图:
里面出现次数最多单词的就是 Tablespace
表空间。下篇文章,我们就来看看表空间结构。等Innodb的结构学习完了,就可以开始另一个层级的学习,即事务、锁等。再往后,就可以来学习分布式数据系统了。