第9章存放页的大池子-InnoDB的表空间

表空间只是一个概念而已,对应的不过就是系统中的一个或者多个实际的文件

独立表空间 -》 表名.ibd文件

表空间就是切分成很多页的数据池子,插入记录的时候就是从池子中捞一个页然后将数据存进去

用户记录 -》 表空间 -〉 对应的页

——————————————————————————

复习之前的知识

页类型

InnoDB存储引擎是以页来进行管理存储空间的。聚簇索引(完整的表数据)和其他的索引比如二级辅助索引都是以B+树的形式保存在表空间中。B+树的节点就是数据页

个人思考:那么这样来说的话,表空间实际上存储着很多B+树的索引

表空间 -》 B+树 -〉 页 -》 记录

可以说表空间就是一本书,书的目录就是B+树的内节点,通过这个目录可以找到具体的数据页,也就是书的页!然后书页中每一行内容就是用户插入的一条一条的记录!

书 -》 目录 -〉 具体书中的哪一页 -》 页中的记录

Man!!这样的逻辑就很清晰了

每个数据页的类型: FIL_PAGE_INDEX

常见的页类型:

——————————————————————————

页通用的部分

数据页,也就是INDEX索引页是由7个部分组成的,其中有两个部分是所有类型的页都通用的部分:

File Header 和 File Trailer,也就是通用信息 + 页数据的校验

File Header的具体组成:

几个需要注意的重点:

(1)每一个页都有一个页号

(2)某些类型的页可以组成一个链表,不用严格按照物理顺序存储

——————————————————————————

独立表空间结构

区的概念:

我们知道表空间是管理的很多页面的一个集合,为了更好地管理这些页,则需要我们设定一个区来分区域进行管理

64个连续的页组成一个区,表空间可以看成是有很多区组成的

每256个区被划分为一组

有点像是我们平时的行政管理单位的划分:

表空间 -》 组 -〉 区 -》 页

组的头几个页都是类似的:

第一组的三个页是固定的:记录整个表空间的一些信息:FSP-HDR , IBUF-BITMAP信息, INODE信息

然后后面的所有的组的开头两个页是固定的:

XDES + IBUF-BITMAP

细节先不用开始就抓的很紧,先把握整体的结构:

总结:还就是, 表空间 -》 组 -〉 区 -》 页

——————————————————————————

段的概念:

如果数据量很少的话,确实不用使用区的概念。但是如果记录越来越多,那么页也会越来越多,所以需要区来辅助管理

虽然B+树中每一层都会形成一个双向链表,从这个理论上来说用不用区还是组的概念都对存储引擎的运行没有影响——也就是完全使用页的概念来管理数据

但是链表中的两个页不一定是连续在物理空间中的,如果两个页的物理位置隔得很远,那么就需要使用随机I/O来找数据了,这样效率不高

为了尽量让链表中的相邻的页的物理位置也相邻,那么才引入了区的概念——尽量查询的时候使用顺序I/O扫描磁盘

一个区就是物理位置上连续的64个页,数据量大的时候,为某个索引分配空间就是直接使用的区来进行划分。浪费是显而易见的,但是性能好处更多:可以消除很多随机的I/O

但是如果将内节点页和数据页一起放到区中,那么扫描效果还是不怎么样

于是设计师将两种节点分别做了处理,内节点由自己的分区,然后数据页节点也有自己的区

存放叶节点的区的集合称之为一个段

存放内节点页的区的集合也是一个段。

一个B+树有两种节点,一个内节点一个叶节点。所以一个索引会生成两个段:内节点段和叶节点段

个人总结:也就是很多区的集合对应一个段,然后不同的区会有不同的段:内节点/叶节点

为了连续的I/O顺序读取:引入64个连续页作为一个区的概念;为了避免内节点和叶节点的混用:引入段的概念作为两种节点页的集合

默认情况下InnoDB存储引擎存储的表有一个主键B+树,也就是一个索引对应两个段,段是以区间为单位申请空间的,一个区默认占用1mb的内存。

那么添加一个索引就要至少要占用2mb的存储空间。记录较少的时候比较浪费

于是这个时候设计师提出了碎片区的概念,也就是一片空闲空间。灵活自由,不属于两种段,而是直接属于表空间

这个是一种针对小记录的存储策略:

插入数据的时候一开始是先从碎片区中以页的单位来分配空间的。后续某个段占用满了32个碎片区,那么就会用完整的区单位来分配存储空间。

这样就将一个很大很笨的段灵活拆解成了完整的区 + 零散的页,这样就避免了过多的浪费了

总结:为了避免出现因为几条小记录而开辟一大块段空间的情况,因此将段空间进一步划分成页 + 碎片区来进一步细化空间,避免浪费

——————————————————————————

区的分类

空闲的区:还没有用到这个区中的任何页

剩余空间的碎片区:碎片区中还有可用的页

没有剩余空间的碎片区:表示碎片区中所有的区都被使用了,没有空闲的页

附属于某个段的区:索引可以划分为叶节点和内节点两种段,分别管理不同的区(对应数据页和目录项页)。数据量很大的时候将使用区来作为基本的分配单位

这四种区也就是对应下面的表:(碎片区 + 附属区)

只有FSEG状态的区是附属于某一个段的,其他的三个区都是直属于表空间

设计师还设计了XDES Entry的结构来管理这些区,也就是每一个区都对应一个XDES Entry结构

(1)Segment ID: 唯一的编号,表示分配给某一个具体的段(前提是只有区已经被提前分配了段这属性才有意义)

(2)ListNode 将很多XDES Entry结构串联成一个表:

两个number用于定位表空间的某一个位置,只需要指定页号以及该页中的偏移量

prev + prev offset = 向前一个指针

next + next offset = 向后一个指针

(3)State

表示区的状态。也就是字面意思的属性:具体看前面的4种区的表格

(4)Page State Bitmap:一共128个比特位

这个属性的比特值表示页的位置:第一 + 第二个比特位对应着第一个页,第三 + 第四 对应着第二个页,以此类推:最后两个比特位表示第64个页

两个比特位代表一个页,第一个表示页是否空闲,第二个表示该页没有使用。

总之该属性就是表示一个区中的所有的页的状态是否被使用,类似于布尔类型的判断值

——————————————————————————

XDES Entry链表

目前我们设定了一堆的概念,什么区,段,表空间,碎片区。只是为了提高表插入数据的效率, 但是又不想浪费数据少的时候的空间。

插入数据本质就是向表中的各个索引的叶节点段,非叶节点段插入数据。

现在结合之前的概念重新梳理一遍插入的流程:

策略:一开始插入数据的时候,段是先从某个碎片区以页为单位来分配存储空间的,当32个碎片页已经用完了再开始索取完整的区的空间

也就是先利用零碎的页,再是完整的区

先利用碎片区中的零碎的页:此时XDES主要针对的是碎片区的页来管理:

(1)段中数据较少的时候,先看表空间中是否有空闲空间的碎片区,如果有的话就将数据插入到这个碎片区中。没有就到下一个FREE区取一些新的零碎的页插入数据,直到区中没有空闲空间

插入之前判断区的状态很重要,核心在于找到FREE状态的区,如果分区很多,那么查询的时候一个一个地找然后判断状态非常慢,于是这个时候XDES Entry链表就派上用场了

XDES Entry中的指针:将不同状态的区连接成三条链表来方便查询

FREE、FREE-FRAG、FULL-FRAG

具体就是空闲区域,空闲碎片区和满状态区

这样每次想找一个FREE-FRAG状态的区的时候,就直接将FREE-FRAG链表的头节点拿出来,从该链表头区中取出一些零碎的页来插入数据,用完一个节点的区就将该节点移动到FULL-FRAG链表中,并修改成对应的状态即可。

如果零碎的区中没有多余的节点了,那么就从FREE状态的区中拿一个节点出来继续使用,以此类推···

可以从这里看到插入数据的逻辑是这样的:

先利用零散的Free碎片区的空间,没有的话在申请整的FREE状态区空间,最后用完的区都划分到FULL状态链表中。

总结:也就是这个XDES实际上是对不同状态的区做了一个分类管理,并且在分类的基础上优化了对32个零散的页的检索和使用

优先利有不完整的有零碎空间的区的零碎的页,然后再用完整的FREE区

零散区状态的变化:FREE -》 FREE-FRAG -〉 FULL

(2)前面的32个零散的区页已经被占用了,那么接下来可以申请完整的区空间来使用了

个人思考:MySQL对空间的利用就好像有强迫症一样,比方说你平时使用纸巾,如果你有一点点强迫症的话,那么你想使用一张纸巾的时候会优先从已经开了的一包纸巾中拿一张出来,只有当这剩的一包纸巾是用完了才会开新的一包!!man!!非常形象了!!

对于完整的区的空间利用,也是从链表分类管理三个不同的区来实现的:

FREE、NOT-FULL、FULL

FREE表示一个完整的区,其中所有的页都是空闲的

NOT-FULL则表示还剩余空间的区

FULL表示完全使用满的区

注意:每一个索引都会使用两个段,然后每一个段都会维护上面的三个链表

并且一个表对应的表空间还需要维护零散的直属于表空间的三个链表(也就是32个零散的区页的空间链表)

——————————————————————————

链表基节点

链表很好的设计,但是如何找到头节点或者尾节点在表空间的位置呢?

这个东西当然设计了:叫做List Base Node: 链表的基节点

每一个链表都有一个基节点:

将基节点放置在表空间中的固定位置,这样就可以快速定位到这个链表了

——————————————————————————

链表小结:

表空间由若干个区域(碎片区 + 完整区,优先利用碎片区)组成的,每一个区都有一个XDES Entry结构,碎片区直属于表空间(FREE、FREE-FRAG、FULL-FRAG:三条链表管理的)

每个完整的区则是由段管理的,也有XDES结构来分类:FREE、NOT-FULL、FULL三个链表

每个链表都有一个List Base Node结构

正是因为链表的存在管理区才更加方便了

个人总结:链表(XDES Entry)的存在本质就是为了更好地管理不同的区,方便用户在存放数据的时候快速从合适的区中拿一个出来给用户作为存放空间

——————————————————————————

段的结构

段不是具体存在的某一个物理上的存储结构,而是由若干的区组成的一个概念

和区有一个XDES一样,设计师也设计了INODE Entry结构来记录段中的属性

Segement:就是段的编号

NOT-FULL-N-USED: 已经使用了多少个页,下次链表分配空闲页的时候直接从这个字段中定位

三个 List Base Node: 也就是完整区的三个状态链表:FREE、NOT-FULL、FULL链表,从段中可以直接定位到它们的位置(从这里也可以看出段是直接管理完整区的,完整区直属于段)

Magic Number:用来标记INODY Entry 是否已经被初始化了(字段是否有值)如果值为97937874,那么代表已经初始化了

Fragment Array Entry:段是一些零散的页和完整的区的组合。每个这个Fragment Array Entry结构都对应着一个零散的页,一共4个字节,表示一个零散的页号

——————————————————————————

各个类型的页的详细情况

我们知道了区有XDES来管理,段有INODE来管理,然后还有直属于表空间的区也有XDES来管理。但是这些结构在表空间的哪里呢?直属于表空间的三个链表的基节点又在哪里呢?

害的从页类型来分析:

——————————————————————————

FSP-HDR:

第一个组的第一个页,也就是页号为0的页,存储了整体表空间的属性和第一个组的256个区对应的XDES结构

也就是说XDES结构是存储在页中的

属性中有两个重点:

(1)File Space Header:表示存储空间的一些整体的属性

其中就有三个直属表空间的基节点,这样就可以快速定位了!(碎片区的链表的基节点位置)

(2)XDES Entry:就是在表中的第一个页保存着呢。但是页的大小是有限的,因此需要将256个区划成一组,每组的第一个页来存放XDES结构。

这就是为什么需要再划分组来管理页了,组不过就是XDES的载体。实际上XDES才是管理的核心

一个组对应256个区,一个XDES管理一个区,因此一个组的第一个页可以放256个XDES来管理这些区

总结:划分成组的原因就是:集中存放区的XDES链表结构,相当于是快速管理属性的一个集合

——————————————————————————

XDES类型

XDES和FSP-HDR唯一的区别就是,FSP是一个表空间的第一个页,除了存放XDES结构还有其他整体的信息保存在FSP这一页,XDES则是在每一组的第一页存放着。它们两个其实是同一种类型的页

XDES :每组的第一页

FSP: 每个表空间的第一页,只是比XDES多了点整体信息

总结:段管理完整区:段存放着完整区的中的链表(XDES)位置。而只属于表空间的碎片区的基节点则是直接放在每一个表空间的第一页中的位置

碎片区: 表空间第一页(基节点位置) -》 XDES -〉 碎片区链表

完整区: 段(存放着链表基节点位置) -》 每组的第一页 -〉 XDES -》 完整区链表

——————————————————————————

IBUF-BITMAP类型

每个组的第一页是XDES,第二页就是这个玩意,记录了一些关于Change Buffer的东西

——————————————————————————

INODE类型

顾名思义,前面的XDES类型的页是存放XDES结构的,那么这个INODE类型的页就是存放INODE结构的

每个分组的第三页就是INODE页喔!

重点是该页的INODE结构和该结构的节点位置

INODE结构是存放段内零散页的地址以及附属于该段的FREE、NOT-FULL、FULL链表的基节点(也就是完整区的链表的基节点的位置):段就是管理完整区的。

节点则是段在链表的节点位置,因为一个表空间中可能会有很多个段,因此需要额外再设计一个INODE类型的页来存储段结构(存放很多种类型段的页)

注意区分:

段结构:是XDES链表基节点的位置

INODE节点:段节点的位置,相当于每个段类型页都是独立的一个一个的节点。注意和基节点区分

为了管理这些INODE页,还是使用链表来管理,一共有两种链表:

SEG-INODES-FULL:没有额外空间的链表

SEG-INODES-FREE:还有剩余的空间

这两个链表就存储在File Space Header,也就是第一个页中的位置

所以以后创建新的段的(创建索引就会创建新的段)时候,还是直接从空余链表中获取节点,然后再将申请到的节点状态修改为已占用:放到FULL链表中

如果没有FREE链表可以用了,那么重新从表空间中申请一张页,然后修改改页的类型为INODE即可,然后将INODE结构存放到这个页中,并将这个页放到FREE链表中

也就是一个表空间通过这些特殊类型的页串起来了!

首先是分组,将很多区分成一组,一个表空间中有很多分组。每一组的开头存放着改组对应的区的管理单位:XDES管理的是不同的区,INODE则是存放管理XDES位置的一个单位。

INODE页 -》 存放 XDES位置 -〉 通过XDES定位到各种(不同类型的)区

然后每组的INODE页又统一放在另两个链表中进行管理,两个链表的位置又是放在表空间的第一页中的

个人总结:

首先是完整区的管理: 表空间 -》 管理段链表位置(通过这个链表定位到不同的段页) -〉 段类型页再存放XDES链表位置(继续递进定位) -》 XDES链表管理不同的区

如果是不完整的碎片区,那么直接走第一页来管理:FSR存放三个碎片区的链表位置 -》 碎片区链表XDES -〉 管理碎片区

进一步提炼: 表空间(FSR) -》 段页链表位置 -〉 链表定位到段页,段页再管理定位XDES -》 XDES再管理完整区 -〉 完整区中的完整页

表空间(FSR) -》 碎片区(三个直属链表) -〉 XDES -》 碎片区 -〉 碎片页

就是一个层层递进的关系:空间 -》(段) -〉区 -》 页

它们之间的联系是通过链表以及链表的基节点位置来实现的

——————————————————————————

Segment Header 结构的运用

一个节点产生的两个段都会对应一个INODE Entry结构,如何知道某个段对应哪个INODE Entry结构呢?

这个对应关系信息是存储在Page Header部分的,也就是数据页的Page Header部分:

然后这两个信息又对应一个Segmen Header结构:

PAGE_BTR_SEG_LEAF记录着叶节点段对应的INODE 结构的地址在表空间中的某个页的偏移量

PAGE-BTR-SEG-TOP则是表示的非叶节点段的位置偏移量

通过地址将索引和段的关系建立起来了,一个索引对应两个段,因此只需要在根节点记录即可

——————————————————————————

真实表空间对应的文件大小

一开始占用的空间不大,新建一个表的表空间文件才占用96kb,但是后续随着数据量的增大占用的空间会越来越大的

——————————————————————————

系统表空间

系统表空间和独立的表空间大体的结构是一样的,只是额外多记录一些有关于整个系统的系统信息页。

——————————————————————————

系统表空间的整体结构

就是比独立表空间的开头处多了记录整个系统的属性页:

——————————————————————————

InnoDB数据字典

除了存储用户数据,还需要存储一些额外信息,比如用户使用了SQL语句,则MySQL需要使用这些额外信息来验证SQL语句的语法是否有问题,还要检查对应操作的表是否存在等等

这些统称为元数据

MySQL的InnoDB存储引擎定义了一些系统内部的表来记录这些数据:

这些系统表就是数据字典了,也是以B+树的形式保存在系统表空间中的某些页中。

其中TABLE、COLUMNS、INDEXES、FILEDS最重要,称之为基础系统表

比方说:

通过TABLE定位到某个具体的表,然后获得到这个表的ID,就可以到COLUMN中获取到这个表中所有的列

通过表ID还可以到INDEXES中拿到这个表的所有索引以及索引ID

索引ID又可以访问到FILEDS中所有的索引列的信息

TABLE -》 获取某个TABLE-ID -〉 获取所有列 + 获取所有索引

也就是有了这4个表你可以获得所有的信息,这4个表是最顶级的表了。

个人思考:这个道理就和之前学习计算机网络的时候DNS解析一样,从顶级的域名服务器开始查询,一步一步获取到某个具体的服务器地址

但是一开始的DNS服务器地址是内置在解析器中的

对应这里的道理一样,如何获取到这4个顶级的表呢?也是一开始就写在代码中的

——————————————————————————

总结:

主要还是梳理清楚表空间的结构即可:

表空间(INODE链表) -》 组(XDES链表 + INODE节点) -〉区(XDES节点) -》页

从管理角度来看

完整区:表空间管理段的链表 -〉 段INODE存储着XDES的位置 -》 通过XDES链表管理区 -〉 完整页

碎片区: 表空间直接管理碎片区的三个链表(存放着基节点在FSR) -》 XDES -〉 碎片区

XDES结构和INODE结构是可以有很多的,一张页放不下,因此需要划分成组,在每组的开头定义XDES页和INODE页进行管理

XDES页存放管理本组所有区的XDES,INODE页存放着本组段节点的位置以及INODE结构(重点是XDES链表位置):

INODE -》 XDES

然后INODE又是交给上层的表空间管理的:

表空间 - 〉 INODE -》XDES

然后将组进一步划分成区是为了尽量使得存放的数据在连续的页中,避免过多的随机I/O扫描磁盘

但是页也分数据页和保存数据页位置的目录项记录页,也就是叶节点和内节点的区分

于是又定义了不同的段分别来管理不同的类型的节点,也就是不同类型的区

总之定义的额外的存储结构概念是为了更好的管理数据和优化存储

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐