在上一章节中,我们知道了提到了表空间,表空间里面没有直接存放表,有的是许多许多的页,我们也说了表空间的分类,有系统表空间,独立表空间,其他表空间。
每个新建的表都会在对应的数据库目录里面添加一个 表明.ibd 的文件,这个文件里面存储了表中的数据索引。一旦我们想要位某个表插入一条记录的时候,就从池子里面捞出来一个对应的页来把数据给写进去,本节内容深入到表空间中,了解各个细节。
复习一些知识
页有很多类型,我们之前说的是数据页,也叫索引页,也叫INDEX类型的页,INDEX类型的页由7个部分组成,其中的两个部分是所有页的通用的,分别是File Header ,File Trailer ,File Trailer 用来检验页是否完整,保证从内存到磁盘刷新时内容的一致性,File Header是用来记录页的一些基本信息,具体如下:
注意事项一:FIL_PAGE_OFFSET ,表示的页号,由4个字节组成,也就是4*8 = 32个二进制位,所以可以有2 的32次方个页,一个页是16kB,那么一个表空间最多是64TB,表空间的第一个页的页号是0,之后的页分别是1,2,3,4,5等等。
注意事项二:某些类型的页可以组成链表,链表中的页可以不按照物理顺序存储,而是根据FIL_PAGE_PREV和FIL_PAGE_NEXT来存储上一个页和下一个页的页号。需要注意的是,这两个字段主要是为了INDEX类型的页,也就是说我们之前一直说的数据页建立B+树之后,为每层节点建立双向链表用的,一般类型的页是不适用这两个字段的。
每个页的类型是由FIL_PAGE_TYPE表示的,不同类型的页,在该字段上面的值是不同的。
什么是区?什么是组?区是为了解决什么问题?组又是为了解决什么问题?
表空间中的页太多了,为了更加方便的管理这些页,区就出现了。 连续的64个页就是一个区(extend),一个组占用的1MB的空间。 无论是系统表空间还是独立表空间,都可以看做是若干个区组成的。 每256个区被划分为一个组
每个组的头几个页的类型都是相似的
右边的每一行都是一个页,之前我们听说过有好多类型的页,但是只学过INDEX页,现在除了数据页他们都出来了
表空间被划分为许多连续的区,每个区默认由64个页组成,每256个区被划分为一组,每个组的最开始的几个页的类型是固定的。
回到最初的问题,区是为了解决什么样的问题?
我们先来分析一下之前没有区的时候,InnoDB是如何查找目录页的?是如何插入数据的?插入数据的本质上就是向该表的聚簇索引以及所有的二级索引代表的B+树节点中插入数据。 而B+树每一层的页都会形成一个双向链表,如果是以页为单位来分配存储空间的话,双向链表相邻的两个页之间的物理结构可能离的非常远。 我们介绍B+树索引使用场景的时候,特别提到范围查询只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,但是如果两个页物理位置相连的太远了,那就是所谓的随机I/O,随机I/O 是非常慢的,所以我们应该让B+树同一层的相邻节点 在物理位置上面也相邻,这个时候进行范围查询的时候才可以使用所谓的随机I/O。
所以,我们才引入了区,不要忘记了区的定义,区是连续的64个页。 在表中数量很大的时候,为某个索引(B+树)分配空间的时候,直接给该索引以区为单位分配空间,分配区,就是分配连续的页,这样可以消除随机I/O,提高访问速度,虽然可能造成了一点点的空间的浪费(B+树无法填满整个区)。
什么是段?段是为了解决什么难题出现的?
现在我们有了区,进行范围查询的时候,就是对B+树叶子节点中的记录进行查找,范围查询的结果可能是分布在多个不同的页中,也就是多个不同的叶子节点中,这些叶子节点是存放在一个区中的,或者是存放在不同的区中,如果是不同的区,那空间就不是连续的了, 这个时候,我们还要尽可能的降低不连续性,如何降低不连续性? 我们需要把查询的区的数量变少,这样就查询的快一些。 这个时候,我们可以把叶子节点也非叶子节点进行区别对待,也就是说叶子节点有独有的区,非叶子节点有独有的区。 存放叶子节点的区的集合就算是一个段, 存放非叶子节点的区也算是一个段,也就是说,一个索引会生成两个段,一个叶子节点段,一个非叶子节点段。
所以,概况来说,段是为了加快查询速度,具体体现在范围查询的时候,数据分散在不同的区中,区是连续的空间,不同的区可能是不连续的,区中的存储是很纯粹的,所以我们可以把区给分开,只把存放叶子节点的区放在一起,这样通过双向链表查询的时候,速度会加快,放在一起的这些区就组成了一个段。
默认情况下,使用InnoDB的存储引擎只有一个聚簇索引,一个索引有两个段,段是以区为单位申请存储空间的,一个区默认1M空间,所以默认情况下一个只存储了几条记录的小小的表也要占用2M的空间吗? 以后每次为这个表添加一个索引都要多申请2M的存储空间么? 这对于存储记录比较少的表,对空间的浪费比较严重。 但是问题出现在,我们现在的区都是十分纯粹的,一个区被整个分配给某一个段,也就是说区中的所有页都是为了存储同一个段的数据而存在的,这样即使段中的数据填不满区中的页,也不能挪作他用。
于是InnoDB的大佬提出了碎片区的概念,那么碎片区的概念就很明显了。在该区的页中,这些页不是为了存储同一个段的数据而存在的,有的页存储了段A,有的页存储了段B,有些页甚至那个段都不属于,碎片区直属于表空间,不属于任何一个段。 所以此后为某个段分配存储空间的策略是这样的:
在刚开始向某个表中插入数据的时候,也就是向该表对应的B+树插入数据的时候,该B+树对应的两个段是从碎片区的某个页为单位进行存储空间的。
当某个段已经占用了32个碎片区之后,就会以完整的区为单位来分配空间。
现在段不能简单定义为某些区的集合,更加准确的应该是某写零散的页,以及一些完整的区的集合。 除了索引的叶子节点段和非叶子节点段之外,InnoDB中还有一些存储特殊数据而定义的段,比如说回滚段等等。
区有哪些分类?为什么要给区分类?
表空间是由段组成的,段中存放的某些区的集合,以及一些零散的页,这些区可以分为4种类型:
空闲的区:到现在为止还没有用到这个区中的任何页。
有剩余空间的碎片区:表示碎片区还有可用的页。
没有剩余空间的碎片区:该碎片区的所有的页都被使用了,没有空闲的页。
附属于某个段的区:每个索引都可以分为叶子节点段和非叶子节点段,除此之外,InnoDB还会有另外一些特殊作用的段,这些段的数据量很大的时候将使用区作为基本的单位分配。
我们再来看一下表空间的结构,牢记区在表空间中是如何存放的
我们看到图片上面的XDES,这是一个页,是和数据页同等级的页,大小是16k; 除了第一个组,后面的每个组的256个区(一个区有64个页)都有XDES页,全称是extent descriptor,用来登记本组的256个区的属性,所以这个页就是管理自己所在区的,所有的区都有这个页。
我们之前分析过数据页,现在我们来详细看一下XDES Entry
这个东西不是一个页,所以页就没 File Header和File Tralier了,这个东西只是一个结构体XDES Entry,是页中一部分,每个区都对着一个此结构。
Segement ID 表示该区所属与某个段的id,只有该区被分配了段该字段才会有意义。
List Node:这个部分可以讲若干个XDES Entry串联成一个链表
表空间是中存放的是组,组中是区,区中是页,页中有XDES,那么所有页中的XDES 如果链接成一个链表的话,那么如果我们想要定位表空间中的某一个位置的话,只需要指定页号以及该位置在指定页号中的页内偏移量即可。
Pre Node Page Number 和 Pre Node Offset 的组合就是指向前一个 XDES Entry的指针。
Next Node Page Number 和 Next Node Offset 的组合就是指向后一个的 XDES Entry的指针。
State 这个字段表明区的状态,可选值就是上面的四个 FREE , FREE_FRAG , FULL_PRAG和FSEG 。
Page State Bitmap
这个部分共占用16个字节,也就是128个比特位。我们说一个区默认有64个页,这128个比特位被划分为64个部分,每个部分2个比特位,对应区中的一个页。比如Page State Bitmap
部分的第1和第2个比特位对应着区中的第1个页,第3和第4个比特位对应着区中的第2个页,依此类推,Page State Bitmap
部分的第127和128个比特位对应着区中的第64个页。这两个比特位的第一个位表示对应的页是否是空闲的,第二个比特位还没有用。
定位表空间的中的某个位置干嘛呢?也就是说组成这个链表是干嘛用的?
到现在为止,我们已经了解了区,段,碎片区,XDES等等的概念,不要忘记了我们最初的目的,只是想插入一条数据的时候保证效率又不至于数量少的表浪费空间。现在我们知道了向表中插入数据,本质上就是想表中的各个索引的叶子节点段,非叶子节点段插入数据,不同的区有不同的状态。 现在我们重新捋一下插入数据的过程:
1. 当段中数据较少没有占满32个页的时候,先查看表空间中是否有状态为FREE_FRAG的区,如果找到了,那么从该区里面取出一些零碎的页插入进去。 否则的话,到表空间中申请一个状态为Free的区,然后让该区变成FREE_FRAG的区,然后从该新申请的区把一些零碎的页把数据给插入进去。 之后不同的段使用零碎页的时候都会中该区中取出数据,直到该区没有零碎页了,那么把该区变成FULL状态。
此处有个问题,就是我们如何知道表空间里面哪些区是FREE的,哪些区是FREE_FRAG的,哪些区是FULL_FRAG的?表空间的不断增大的,当增长到GB的时候,区的数量也就上千了,我们不能每次从头遍历这些区对应的XDES Entry结构 的State字段来进行判断吧? 那是如何定位空闲区的呢? 这个时候 XDES Entry 中的List Node 部分就发挥效果了,我们通过 List Node中的指针,做三件事情:把状态是FREE的区对应的XDES Entry结构通过 List Node连接成一个链表,叫做FREE链表;同理FREE_FRAG链表;同理 FULL链表。
这样每当我们想找一个FREE_FRAG
状态的区时,就直接把FREE_FRAG
链表的头节点拿出来,从这个节点中取一些零碎的页来插入数据,当这个节点对应的区用完时,就修改一下这个节点的State
字段的值,然后从FREE_FRAG
链表中移到FULL_FRAG
链表中。同理,如果FREE_FRAG
链表中一个节点都没有,那么就直接从FREE
链表中取一个节点移动到FREE_FRAG
链表的状态,并修改该节点的STATE
字段值为FREE_FRAG
,然后从这个节点对应的区中获取零碎的页就好了
2. 当段中数据已经占满了32个页了,那么之后的话直接是以区为单位进行空间申请。
此处有个问题,当我们插入之前,我们怎么知道哪个区是属于哪个段的呢? 我们总不能拿那些已经归属于某个段,状态为FSEG的区来使用吧。 所以我们把FSEG 的区对应的XDES Entry结构都独立的加入一个链表中? 这样是不行的,不同索引的不同的段是不能共用同一个区的。 我们需要每个段都有他独立的链表,所以我们可以根据段号(Segment ID)来建立链表,与此同时,一个段里面的区也有好多,有时完全空间的,有慢的,有处于两者之间的,所以每个段里面的区,根据XEDS Entry结构建立了三个链表,FREE链表,NOT_FULL链表,FULL链表。
如何找到这些链表呢?什么是链表基节点呢?链表基节点存储在哪个地方呢?
我们上面介绍了很多的链表,但是如何找到这些链表呢? 如何找到这些链表的头结点或者是尾节点呢? 其实是通过List Base Node 结构体找到的,结构体如下
我们上面说的每一条链表都有一个对应的这样的结构,并且放在固定的位置,那我们就很容易定位到上面所说的所有链表了。
List Length :该链表的长度
First Node Page Number 和 First Node Offset 表明该链表的头结点所在表空间的位置
Last Node Page Number
和Last Node Offset
表明该链表的尾节点在表空间中的位置
至于链表基节点存储在哪里,我们后面再说。
段的结构是什么?
我们前面对段的定义是,若干个零散的页以及一些完整的区,段不是表空间中的连续的物理区域,是一个逻辑上的概念。 就像是每个区都有对应的XDES Entry 来记录这个区的属性一样,InnoDB的官方也设计了每个段都有一个 INODE结构来记录段中的属性一样
Segment ID:段的id
NOT_FULL_N_USED:NOT_FULL链表已经使用了多少个页。 下次我们需要从NOT_NULL链表中分配空闲页的时候可以直接根据这个字段定位到。不用从链表第一个页开始查找了。
3个List Base Node:分别对应FREE链表,NOT_FULL链表,FULL链表 的基节点,可以快速定位到某个段的某个链表的头尾节点。
Magic Number :用来标记 INODE Entry 是否已经被初始化。
Fragment Array Entry : 每个Fragment Array Entry
结构都对应着一个零散的页,这个结构一共4个字节,表示一个零散页的页号。
上面我们分析了各种的结构,但是我们始终不知道这些结构都是在哪个地方存放的,现在我们来看一下吧。
FSP_HRD 类型
第一个组的第一个页就是这个类型,这是我们除了index页之后的新页,它里面存储了表空间的整体属性和第一个组里面的256个区对应的XDES Entry 结构。
File Header和File Trailer就不用再说了。
File Space Header 用来存储表空间的整体信息。
XDES Entry 存储本组的256个区的属性信息
Empty Space 顾明思议,空的空间
重点来看File Space Header ,用来存储表空间的整体属性,看下具体结构
Space ID : 表空间id
Not Used : 未被使用
Size : 当前表空间占用的页数
FREE Limit 尚未被初始化的最小页号,大于或等于这个页号的区对应的XDES Entry 结构都没有被加入FREE链表 。 表空间对应的是具体的磁盘文件,一开始创建表空间的时候对应的磁盘文件都没有数据,所以我们需要对表空间进行一个初始化,包括为表空间中的区建立一个XDES Entry 结构,为每个段建立INODE Entry结构,建立链表等等操作。 我们可以一开始为表空间申请特别大的空间,但是实际上大部分时候里面的所有的区都是空闲的,我们可以把所有空闲的区的XDES Entry 结构加入链表,也可以选择只把一部分空闲区对应的XDES Entry 加入链表,等什么时候空闲链表中的XDES Entry
结构对应的区不够使了,再把之前没有加入FREE
链表的空闲区对应的XDES Entry
结构加入FREE
链表,中心思想就是什么时候用到什么时候初始化,设计InnoDB
的大佬采用的就是后者,他们为表空间定义了FREE Limit
这个字段,在该字段表示的页号之前的区都被初始化了,之后的区尚未被初始化。
Space Flags 表空间中一些占用存储空间较小的属性
FRAG_N_USED FREE_FRAG链表中已经使用的页的数量
List Base Node for Free List :表空间的FREE链表基节点
List Base Node for FREE_FRAG List :FREE_FREG链表的基节点
List Base Node for FULL_FRAG List :FULL_FREG链表的基节点
Next Unused Segment ID : 当前表空间中下一个未使用的 Segment ID ,方便为之后的表空间初始化赋值。
List Base Node for SEG_INODES_FULL List :SEG_INODES_FULL链表的基节点
List Base Node for SEG_INODES_FREE List SEG_INODES_FREE链表的基节点
XDES Entry 部分
学了半个这个结构,现在才知道,原来这个结构是表空间中的第一个页里面存储的,第一个页里面会存储256个XDES Entry,分别对应改组里面的256个区。
XED类型
这个类型和上面的FSP_HRD相似,除了少了关于表空间的 File Space Header部分
IBUF_BITMAP 类型
暂时省略
INODE类型
此类型只位于第一个组的第一个区的第三个页中,里面存储的是关于表空间中的段的信息。 每个段都有一个INODE Entry 结构 。 我们看下一INODE 类型页的结构
INODE Entry 部分,已经介绍过了,存储的是零散的页的地址以及属于该段的FREE,NOT_FULL,FULL链表的基节点。
List Node For INODE Page List :