在InnoDB的存储引擎中。表空间是一个抽象的概念。,对于系统表空间来说,对应着文件系统中一个或多个 实际文件;对于每个独立表空间来说,对应着文件系统中一个名为 表名.ibd 的实际文件。我们可以把表空间当成被切分为许许多多个页的池子。插入一条数据的时候。从池子中捞出一个对应的页面把数据写进去。(如果这块对InnoDB的页不太了解的话可以看一下我之前写的关于页的文章哦! 看不懂打我!!!!)
介绍表空间之前。先准备点基础知识吧
# 页面类型
都知道InnoDB是以页为单位管理存储空间的。我们的聚簇索引和其他的二级索引都是以B+树的形式保存到表空间的。而B+树的结点就是数据页(也就是索引页 一个概念)这个数据页的类型是FIL_PAGE_INDEX。除了存放数据的页面类型之外。InnoDB也为了不同目的设计出了不同类型的页面
类型名称 | 十六进制 | 描述 |
---|---|---|
第一列文本居中 | 第二列文本居右 | 第三列文本居左 |
FIL_PAGE_TYPE_ALLOCATED | 0x0000 | 最新分配,还没使用 |
FIL_PAGE_UNDO_LOG | 0x0002 | Undo日志页 |
FIL_PAGE_INODE | 0x0003 | 段信息节点 |
FIL_PAGE_IBUF_FREE_LIST | 0x0004 | Insert Buffer空闲列表类型名称 十六进制 描述 |
FIL_PAGE_IBUF_BITMAP | 0x0005 | Insert Buffer位图 |
FIL_PAGE_TYPE_SYS | 0x0006 | 系统页 |
FIL_PAGE_TYPE_TRX_SYS | 0x0007 | 事务系统数据 |
FIL_PAGE_TYPE_FSP_HDR | 0x0008 | 表空间头部信息 |
FIL_PAGE_TYPE_XDES | 0x0009 | 扩展描述页 |
FIL_PAGE_TYPE_BLOB | 0x000A | BLOB页 |
FIL_PAGE_INDEX | 0x45BF | 索引页,也就是我们所说的 数据页 |
# 页面通用部分
在页结构中File Header和File Trailer这俩个部分基本是所有页结构通用的
- File Header 记录页面的一些通用信息
- File Trailer:校验页是否完整。保证从内存到磁盘刷新时内容的一致性。
重新看一下File Header的结构吧
名称 | 大小 | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4 字节 | 页的校验和(checksum值) |
FIL_PAGE_OFFSET | 4 字节 | 页号 |
FIL_PAGE_PREV | 4字节 | 上一个页的页号 |
FIL_PAGE_NEXT | 4 字节 | 下一个页的页号 |
FIL_PAGE_LSN | 8 字节 | 页面被最后修改时对应的日志序列位置(英文名是:Log Sequence Number) |
FIL_PAGE_TYPE | 2 字节 | 该页的类型 |
FIL_PAGE_FILE_FLUSH_LSN | 8 字节 | 仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4 字节 | 页属于哪个表空间 |
- 表空间当中每个页都对应着一个页号。
- 某些类型的页可以组成链表,链表中的页可以不按照物理顺序存储。而是根据 FIL_PAGE_PREV 和 FIL_PAGE_NEXT 来存储上一个页和下一个页的页号。这俩个字段主要是给FIL_PAGE_INDEX类型的页使用的。建立B+树后。为每一层建立双向链表。
# 独立表空间结构
在InnoDB存储引擎中有很多很多的表空间。本文我们主要介绍一下独立表空间。
# 区(extent)的概念
因为表空间的页实在是太多了。因此InnoDB设计了区(extent)的概念。对于16kb的页来讲。连续的64个页就是一个区。也就是一个区默认占用1MB空间大小。不论是系统 表空间还是独立表空间,都可以看成是由若干个区组成的,每256个区被划分成一组。如图所示
其实extent-0~entent-255这个256个区算是第一个组以此类推。这几个组的头几个页面类型是相似的。如果我们把表空间结构在缩小来看的话是这样的
第一个组最开始的3个页面的类型是制定的。也就是extent0这个区最开始的3个页面的类型是固定的。
- FSP_HDR类型:这个类型的页面是用来登记整个表空间的一些整体属性以及本组所有的区,也就是extent 0 ~ extent 255 这256个区的属性。整个表空间只有一个FSP_HDR类型的页面
- IBUF_BITMAP 这个类型的页面是存储本组所有的区的所有页面关于INSERT BUFFER的信息(这个本文就不做详细解释了。)
- INODE:这个页面存储了许多称为INODE类型的数据结构
其余各组最开始的2个页面的类型是固定的
- XDES类型:用来登记本组256个区的属性。
- IBUF_BITMAP 独立表空间的宏观结构就介绍到这了
# 段(segment)的概念
为啥好端端要提出区的概念呢?我们都知道在InnoDB存储引擎下表中的记录都是存储到页里面的。然后页作为结节点组成B+树。这个B+树就是索引。如果表中的记录数量很少的话确实用不到区的概念。可是如果很多呢。我们每向表中插入一条记录,本质上就是向该表的聚簇索引以及所有二级索引代表的 B+ 树的节点中插入数据。而 B+ 树的每一层中的页都会形成一个双向链表。如果只用页为单位来存储空间的话。双向链表中相邻的两个页可能离的就特别特别远,这种情况如果我们去进行范围查询的话就会产生随机IO。随机IO是非常非常慢的。这个时候我们提出了区的概念。一个区就是在物理位置上连续的64个页。这样进行查询的时候就是顺序IO了。
范围查询,其实是对 B+ 树叶子节点中的记录进行顺序扫描,而如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请到的区中的话。这样效果就大打折扣了。所以InnoDB对B+树的叶子节点和非叶子节点进行了区别对待。也就是说叶子节点有自己独有的 区 ,非叶子节点也有自己独有的 区 。存放叶子节点的区的集合就算是一个 段 ( segment ),存放非叶子节点的区的集合也算是一个 段 。也就是说一个索引会生成2个段,一个叶子节点段,一个非叶子节点段。
InnoDB默认情况下一个表只有一个聚簇索引,一个索引会生成2个段,而段是以区为单位申请存储空间的。一个区默认占用1M存储空间。但是如果一个表中存储的数据非常少的话也要为这个表生成2个段嘛。那肯定不是的。InnoDB提出了碎片区的概念,也就是在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的。 碎片区中的页用于不同的目的,比如有些页用于段A,有些页用于段B,有些页甚至哪个段都不属于。碎片区直属于表空间,并不属于任何一个段。
因此为某个段分配存储空间是这样的:
- 在刚开始向表中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的
- 当某个段已经占用了32个碎片区之后。就会以完整的区为单位分配存储空间。
所以段更准确来讲,是某些零散的页面和一些完整的区的结合。除了叶子结点段和非叶子结点段。还有一些为特殊数据而定义的段,比如回滚段等等。
这块可能读起来有点烦躁。可是知识就是这样。当你耐心读下来的时候就会收获更多
# 区的分类
通过上面描述 我们知道表空间是由若干个区组成的。这些区大体上可以分为4类
- 空闲的区,现在还没有用到这个区中任何页面
- 有剩余空间的碎片区:表示碎片区中还有可用的页面
- 没有剩余空间的碎片区:表示碎片区中的所有页面都被使用。没有空闲页面
- 附属于某个段的区:每一个索引都可以分为叶子节点段和非叶子节点段,除此之外InnoDB还会另外定义一些特殊作用的段,在这些段中的数据量很大时将使用区来作为基本的分配单位。
状态名 | 含义 |
---|---|
FREE | 空闲的区 |
FREE_FRAG | 有剩余空间的碎片区 |
FULL_FRAG | 没有剩余空间的碎片区 |
FSEG | 附属于某个段的区 |
处于 FREE 、 FREE_FRAG 以及 FULL_FRAG 这三种状态的区都是独立的,算是直属于表空间;而处于 FSEG 状态的区是附属于某个段的。 为了方便管理这些区,每一个区都对应着一个XDES Entry结构
XDES Entry 是一个40个字节的结构,大致分为4个部分,各个部分的释义如下:
- Segment ID (8字节) 每一个段都有一个唯一的编号,用ID表示,此处的 Segment ID 字段表示就是该区所在的段。当然前提是该区已经被分配给某个段了,不然的话该字段的值没啥意义。
- List Node
- Pre Node Page Number 和 Pre Node Offset 的组合就是指向前一个 XDES Entry 的指针
- Next Node Page Number 和 Next Node Offset 的组合就是指向后一个 XDES Entry 的指针。
- State:表明区的状态, 分别是: FREE 、 FREE_FRAG 、 FULL_FRAG 和 FSEG 。
- Page State Bitmap: 这个部分共占用16个字节,也就是128个比特位。我们说一个区默认有64个页,这128个比特位被划分为64个部分,每个部分2个比特位,对应区中的一个页。这两个比特位的第一个位表示对应的页是否是空闲的,第二个比特位还没有用。
MySQL插入数据的时候是如何在表空间找到不同类型的区的:主要是通过XDES Entry中的ListNode来实现的
- 把状态为 FREE 的区对应的 XDES Entry 结构通过 List Node 来连接成一个链表,这个链表我们就称之为 FREE 链表。
- 把状态为 FREE_FRAG 的区对应的 XDES Entry 结构通过 List Node 来连接成一个链表,这个链表我们就称之为 FREE_FRAG 链表。
- 把状态为 FULL_FRAG 的区对应的 XDES Entry 结构通过 List Node 来连接成一个链表,这个链表我们就称之为 FULL_FRAG 链表。
当段中数据已经占满了32个零散的页后,就直接申请完整的区来插入数据了。
如何知道MySQL哪些区属于哪些段的。MySQL为每个段中的区对应的 XDES Entry 结构建立了三个链表:
- FREE 链表:同一个段中,所有页面都是空闲的区对应的 XDES Entry 结构会被加入到这个链表。注意和直属于表空间的 FREE 链表区别开了,此处的 FREE 链表是附属于某个段的。
- NOT_FULL 链表:同一个段中,仍有空闲空间的区对应的 XDES Entry 结构会被加入到这个链表。
- FULL 链表:同一个段中,已经没有空闲空间的区对应的 XDES Entry 结构会被加入到这个链表。
# 链表基结点
上面我们已经介绍了很多种链表。那么我们是如何找到这些链表的呢?InnoDB设计了一个List Base Node 的结构。这个结构包含了链表的头节点和尾结点的指针以及链表中包含了多少结点的信息。
- List Length 表明该链表一共有多少节点,
- First Node Page Number 和 First Node Offset 表明该链表的头节点在表空间中的位置。
- Last Node Page Number 和 Last Node Offset 表明该链表的尾节点在表空间中的位置。
一般把某个链表对应的 List Base Node 结构放置在表空间中固定的位置,这样想找定位某个链表就变得so easy啦。
# 链表小结
综上所述。表空间是由若干个区组成的,每个区都对应一个XSDES Entry的结构,直属于表空间的区对应的XDES Entry结构可以分成FREE、FREE_FRAG和FULL_PRAG这三个链表,每个段都可以附属若干个区,每个段中的区对应的XDES Entry结构可以分成FREE、NOT_FULL和FULL这三个链表,每个链表都对应一个List Base Node 的结构。这个结构记录了链表的头、尾结点的位置以及该链表中包含的节点数,正是因为这些链表的存在,管理这些区才很容易。
# 段的结构
段其实不对应表空间中某一个连续的物理区域,而是一个逻辑上的概念,由若干个零散的页面以及一些完整的区组成。每个段都定义了一个 INODE Entry 结构来记录一下段中的属性。
它的各个部分释义如下:
- Segment ID: 就是指这个 INODE Entry 结构对应的段的编号(ID)。
- NOT_FULL_N_USED: 这个字段指的是在 NOT_FULL 链表中已经使用了多少个页面。下次从 NOT_FULL 链表分配空闲页面时可以直接根据这个字段的值定位到。而不用从链表中的第一个页面开始遍历着寻找空闲页面。
- 3个 List Base Node: 分别为段的 FREE 链表、 NOT_FULL 链表、 FULL 链表定义了 List Base Node ,这样我们想查找某个段的某个链表的头节点和尾节点的时候,就可以直接到这个部分找到对应链表的 List Base Node 。so easy!
- Magic Number : 这个值是用来标记这个 INODE Entry 是否已经被初始化了(初始化的意思就是把各个字段的值都填进去了)。如果这个数字是值的 97937874 ,表明该 INODE Entry 已经初始化,否则没有被初始化。(不用纠结这个值有啥特殊含义,人家规定的)。
- Fragment Array Entry: 我们前边强调过无数次段是一些零散页面和一些完整的区的集合,每个 Fragment Array Entry 结构都对应着一个零散的页面,这个结构一共4个字节,表示一个零散页面的页号。结合着这个 INODE Entry 结构,大家可能对段是一些零散页面和一些完整的区的集合的理解再次深刻一些。
从宏观上大概看一下独立表空间的各个组成部分
# 各类型页面详细情况
上面我们说了很多概念,表空间、段、区、XDES Entry,还有几个XDES Entry为结点的链表。那么这些东西到底催在表空间的哪里。例如每个区对应的XDES Entry结构到底存储在表空间什么地方等等等等。。。
我们知道InnoDB是以页为基本单位的。每64个页可以称为一个区,每256个连续的区可以算是一个组,其实归根结底来说各种信息还是存在页里面的。
# FSP_HDR类型的页
在独立表空间中第一个组的第一个页面的类型是FSP_HDR,它存储了表空间的一些整体属性以及第一个组内256个区的对应的XDES Entry结构,
这块我们就整体看一下表空间的结构。后续只带大家来看对应的页了。
名称 | 中文名 | 占用空间大小 | 简单描述 |
---|---|---|---|
File Header | 文件头部 | 38 字节 | 页的一些通用信息 |
File Space Header | 表空间头部 | 112 字节 | 表空间的一些整体属性信息 |
XDES Entry | 区描述信息 | 10240 字节 | 存储本组256个区对应的属性信息 |
Empty Space | 尚未使用空间 | 5986 字节 | 用于页结构的填充,没啥实际意义 |
File Trailer | 文件尾部 | 8 字节 | 校验页是否完整 |
File Header 和File Trailer这里就不展开讲了。感兴趣的话可以看我上一篇文章哦。专门讲解了页相关的知识点
- File Space Header:看名字也可以看出来。主要是用来存储表空间的一些整体属性的
名称 | 占用空间大小 | 描述 |
---|---|---|
Space ID | 4 字节 | 表空间的ID |
Not Used | 4 字节 | 这4个字节未被使用,可以忽略 |
Siz | e 4 字节 | 当前表空间占有的页面数 |
FREE Limit | 4 字节 | 尚未被初始化的最小页号,大于或等于这个页号的区对应的XDES Entry结构都没有被加入FREE链表 |
SpaceFlags | 4 字节 | 表空间的一些占用存储空间比较小的属性 |
FRAG_N_USED | 4 字节 | FREE_FRAG链表中已使用的页面数量 |
List Base Node for FREE List | 16 字节 | FREE链表的基节点 |
List Base Node for FREE_FRAG List | 16 字节 | FREE_FREG链表的基节点 |
List Base Node for FULL_FRAG List | 16 字节 | FULL_FREG链表的基节点 |
Next Unused Segment ID | 8 字节 | 当前表空间中下一个未使用的 Segment ID |
List Base Node for SEG_INODES_FULL List | 16 字节 | SEG_INODES_FULL链表的基节点 |
List Base Node for SEG_INODES_FREE List | 16 字节 | SEG_INODES_FREE链表的基节点 |
这里面的描述已经很清楚了。就不做一一介绍了。。有兴趣的同学可以百度一下 哈哈哈 活着chatgpt也行哈 。
# XDES Entry
XDES Entry结构对应表空间的一个区,一个XDES Entry结构的大小是40字节。但是一个页面的大小有限,所以我们才把256个区划分成一组,在组的第一个页面存放256个XDES Entry结构。因为每个区对应的XDES Entry结构的地址是固定的。所以访问的时候就能在对应的页面中直接找到对应的XDES Entry啦。
# XDES类型的页
除去第一个组以外。之后的每个分组的第一个页面只需要记录本组内所有区对应的XDES Entry结构即可。我们把除去第一个组之外的每个分组的第一个页面的类型定义为XDES。它的结构和上面的FSP_HDR是非常类似的。
# IBUF_BITMAP类型
每个分组的第二个页面的类型都是IBUF_BITMAP。这种类型的页面里边记录了一些有关Change Buffer的东西。本文就不做详细介绍了(以后有机会,专门介绍一下)
# INODE类型
第一个分组的第三个页面的类型就是INODE。INODE类型就是为了存储段的INODE Entry结构而存在的。
名称 | 中文名 | 占用空间大小 | 简单描述 |
---|---|---|---|
FileHeader | 文件头部 | 38 字节 | 页的一些通用信息 |
List Node for INODE Page List | 通用链表节点 | 12 字节 | 存储上一个INODE页面和下一个INODE页面的指针 |
INODE Entry | 段描述信息 | 16128 字节 | |
Empty Space | 尚未使用空间 | 6 字节 | 用于页结构的填充,没啥实际意义 |
File Trailer | 文件尾部 | 8 字节 | 校验页是否完整 |
这里就重点看一下List Node for INODE Page List这个玩意,因为一个表空间可能存在超过85个段。所以可能一个INODE类型的页面不足以存储所有的段对应的INODE Entry结构,所以就需要额外的INODE类型的页面来存储这些结构。还是为了方便管理这些INODE类型的页面,InnoDB将这这些INODE类型的页面串联成俩个不同的链表:
- SEG_INODES_FULL链表:该链表中的 INODE 类型的页面中已经没有空闲空间来存储额外的 INODE Entry 结构了。
- SEG_INODES_FREE 链表:该链表中的 INODE 类型的页面中还有空闲空间来存储额外的 INODE Entry 结构了。
这两个链表结构大家是不是有点眼熟。。哈哈啊哈哈 没有 这两个链表的基结点就存储在File Space Header里面。也就是说这两个链表的基结点的位置是固定的。所以我们能很轻松的访问到这两个链表。存储过程就不给大家介绍了。大家脑补一下吧。
# Segment Header结构的应用
一个索引会产生两个段,分别是叶子结点段和非叶子结点段。而每个段都会对应一个INODE Entry结构,那我们如何知道那个段对应哪个结构的??????? 在数据页里面也就是页类型是INDEX 时有一个PageHeader部分。里面就存储了这两个结点。(不知道的同学可以看我上一篇文章哦) 其中的 PAGE_BTR_SEG_LEAF 和 PAGE_BTR_SEG_TOP 都占用10个字节,它们其实对应一个叫 Segment Header 的结构
名称 | 占用字节数 | 描述 |
---|---|---|
Space ID of the INODE Entry | 4 | INODE Entry结构所在的表空间ID |
Page Number of the INODE Entry | 4 | INODE Entry结构所在的页面页号 |
Byte Offset of the INODE Ent | 2 | INODE Entry结构在该页面中的偏移量 |
PAGE_BTR_SEG_LEAF 记录着叶子节点段对应的 INODE Entry 结构的地址是哪个表空间的哪个页面的哪个偏移量, PAGE_BTR_SEG_TOP 记录着非叶子节点段对应的 INODE Entry 结构的地址是哪个表空间的哪个页面的哪个偏移量。这样子索引和其对应的段的关系就建立起来了。不过需要注意的一点是,因为一个索引只 对应两个段,所以只需要在索引的根页面中记录这两个结构即可。
表空间大体就介绍到这里了 可能有的东西还是还是还是有点蒙圈。。。。。这里天准备出一个尽量完整的表空间的数据图 给大家看一下吧。