mysql中InnoDB的表空间--独立表空间

大家好,上篇文章我们在讲mysql数据目录的时候提到了表空间这个名词,它是一个抽象的概念,对于系统表空间来说,对应着文件系统中一个或多个实际文件;对于每个独立表空间来说,对应着文件系统中一个名为表名.ibd的实际文件。我们可以把表空间比作存储页的容器,当我们想为某个表插入一条记录的时候,就从容器中找出一个对应的页来把数据写进去。今天我们就先来深入聊一下InnoDB的独立表空间。

在讲表空间之前,我们先回忆一下页的相关内容,这有助于下面我们对表空间的理解。

页面类型

我们知道InnoDB是以页为单位管理存储空间的,聚簇索引和二级索引 都是以B+树的形式保存到表空间的,而B+树的节点就是数据页。InnoDB除了数据页之外,还有许多不同类型的页,具体如下所示:

类型名称十六进制描述
FIL_PAGE_TYPE_ALLOCATED0x0000最新分配,还没使用
FIL_PAGE_UNDO_LOG0x0002Undo日志页
FIL_PAGE_INODE0x0003段信息节点
FIL_PAGE_IBUF_FREE_LIST0x0004Insert Buffer空闲列表
FIL_PAGE_IBUF_BITMAP0x0005Insert Buffer位图
FIL_PAGE_TYPE_SYS0x0006系统页
FIL_PAGE_TYPE_TRX_SYS0x0007事务系统数据
FIL_PAGE_TYPE_FSP_HDR0x0008表空间头部信息
FIL_PAGE_TYPE_XDES0x0009扩展描述页
FIL_PAGE_TYPE_BLOB0x000ABLOB页
FIL_PAGE_INDEX0x45BF索引页,也就是数据页

因为页面类型前边都有个FIL_PAGE 或者 FIL_PAGE_TYPE 的前缀,为简便起见我们后边讲的页面类型的时候就把这些前缀省略掉了,比如FIL_PAGE_TYPE_ALLOCATED就叫ALLOCATED。

页面通用部分

之前我们了解过,数据页由7部分组成,这其中有两部分是所有类型的页通用的。

在这里插入图片描述

File Header: 记录页面的一些通用信息。
File Trailer: 校验页是否完整,保证页面在从内存刷新到磁盘后内容是相同的。

独立表空间结构

回忆完页的内容后,我们就来讲一下今天的主要内容–独立表空间。在讲表空间之前,我们要了解区和段这两个概念。

区的概念

为了更好的管理表空间里的页,InnoDB提出了区(英文名:extent) 的概念。对于16KB的页来说,连续的64个页为一个区,也就是说一个区默认占用1MB空间大小。不论是系统表空间还是独立表空间,都可以看成是由若干个区组成的,每256个区被划分成一组。如下图所示:
在这里插入图片描述

其中extent0 ~ extent255为第一个组,extent256 ~ extent511这256个区算是第二个 组,extent512 ~ extent767这256个区算是第三个组,依此类推可以划分更多的组。这些组的头几个页面的类型都是类似的,如下图所示:
在这里插入图片描述

第一个组最开始的3个页面的类型是固定的,也就是第一个组第一个区extent0最开始的3个页面的类型是固定的, 分别是:

FSP_HDR 类型: 这个类型的页面是用来登记整个表空间的一些整体属性以及本组所有的区(extent0 ~ extent255)的属性,注意:整个表空间只有一 个FSP_HDR 类型的页面。

IBUF_BITMAP 类型: 这个类型的页面是存储本组所有的区的所有页面关于 INSERT BUFFER 的信息。

INODE 类型: 这个类型的页面存储了许多称为INODE 的数据结构。下面会具体讲。

其余各组最开始的2个页面的类型是固定的,分别是:

XDES 类型: 全称是extent descriptor,用来登记本组256个区的属性。与第一个组的 FSP_HDR 类型的页面其实 和XDES 类型的页面的作用类似,只不过FSP_HDR 类型的页面还会额外存储一些表空间的属性。

IBUF_BITMAP 类型: 与第一个组的IBUF_BITMAP 类型一样。

之前我们讲记录存储过程时,一直说是将表中的记录存储到页中,然后页作为节点组成B+树。如此看来,不引入区的概念只使用页的概念对存储引擎的运行并没啥影响。但是B+树的每一层中的页都会形成一个双向链表,如果是以页为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远。当我们通过B+数索引进行范围查询时,如果链表中相邻的两个页物理位置离得非常远,那么就会产生随机I/O。要知道,在磁盘中随机I/O 是非常慢的。

所以我们应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以使用所谓的顺序I/O。所以才引入了区(extent)的概念,一个区就是在物理位置上连续的64个页。在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区为单位分配,这样就可以消除很多的随机I/O。

段的概念

我们提到的范围查询,其实是对B+树叶子节点中的记录进行顺序扫描,如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请到的区中的话,进行范围扫描的效果就会大打折扣。所以InnoDB 对B+树的叶子节点和非叶子节点进行了区别对待,也就是说叶子节点有自己 独有的区,非叶子节点也有自己独有的区。存放叶子节点的区的集合就算是一个段(segment),存放非叶子节点的区的集合也算是一个段。也就是说一个索引会生成2个段,一个叶子节点段,一个非叶子节点段。

我们刚才介绍的是一个区会被整个分配给某一个段,或者说区中的所有页面都是为了存储同一个段的数据而存在的,即使段的数据填不满区中所有的页面,那余下的页面也不能挪作他用。现在为了考虑以完整的区为单位分配给某个段对于数据量较小的表太浪费存储空间的这种情况,InnoDB提出了碎片(fragment) 区的概念,也就是在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,而是碎片区中的页可以用于不同的目的,比如有些页用于段A,有些页用于段B,有些页甚至哪个段都不属于。碎片区直属于表空间,并不属于任何一个段。

所以为某个段分配存储空间的策略是这样的:在刚开始向表中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的。当某个段已经占用了32个碎片区页面之后,就会以完整的区为单位来分配存储空间。所以现在段不能仅定义为是某些区的集合,更精确的应该是某些零散的页面以及一些完整的区的集合。

除了索引的叶子节点段和非叶子节点段之外,InnoDB 中还有为存储一些特殊的数据而定义的段,比如回滚段。

区的分类

上述介绍的区大体上可以分为4种类型,也被称为4种状态:

空闲的区(FREE ): 现在还没有用到这个区中的任何页面。

有剩余空间的碎片区(FREE_FRAG ): 表示碎片区中还有可用的页面。

没有剩余空间的碎片区(FULL_FRAG ): 表示碎片区中的所有页面都被使用。

附属于某个段的区(FSEG ): 每一个索引都可以分为叶子节点段和非叶子节点段,除此之外InnoDB还会另外定义一些特殊作用的段,在这些段中的数据量很大时将使用区来作为基本的分配单位。

注意:处于FREE、FREE_FRAG以及FULL_FRAG这三种状态的区都是独立的,算是直属于表空间;而处于FSEG 状态的区是附属于某个段的。

为了方便管理这些区,InnoDB设计了一个名为XDES Entry的结构(全称就是Extent Descriptor Entry),每一个区都对应着一个XDES Entry 结构,这个结构记录了对应的区的一些属性。如下图所示:

在这里插入图片描述

XDES Entry共40个字节,大致分为4个部分:

Segment ID (8字节) 每一个段都有一个唯一的编号,用ID表示,此处的Segment ID 字段表示就是该区所在的段。

List Node (12字节) 这个部分将若干个XDES Entry 结构串联成一个链表,Pre Node Page Number 和 Pre Node Offset 的组合就是指向前一个XDES Entry的指针,Next Node Page Number 和 Next Node Offset 的组合就是指向后一个XDES Entry的指针。

State (4字节) 这个字段表明区的状态。也就是上面提到的FREE、FREE_FRAG 、 FULL_FRAG 和FSEG 。

Page State Bitmap (16字节) 这个部分共占用16个字节,也就是128个比特位。我们说一个区默认有64个页,这128个比特位被划分为64 个部分,每个部分2个比特位,对应区中的一个页。比如Page State Bitmap部分的第1和第2个比特位对应着区中的第1个页面,第3和第4个比特位对应着区中的第2个页面。这两个比特位的第一个位表示对应的页是否是空闲的,第二个比特位还没有用。

XDES Entry链表

刚我们提到XDES Entry结构中的List Node(12字节)这个部分将若干个XDES Entry结构串联成一个链表,下面我们再来聊聊XDES Entry链表。

我们先总结一下某个段中插入数据的过程: 当段中数据较少的时候,首先会查看表空间中是否有状态为FREE_FRAG(有剩余空间的碎片区)的区,如果有,那么从该区中取一些零碎的页把数据插进去;否则到表空间下申请一个状态为FREE(空闲的区)的区,把该区的状态变为FREE_FRAG ,然后从该新申请的区中取一些零碎的页把数据插进去。之后不同的段使用零碎页的时候都会从该区中取,直到该区中没有空闲空间,然后该区的状态就变成了FULL_FRAG 。

那么我们怎么知道表空间里的哪些区是FREE的,哪些区的状态是FREE_FRAG 的,哪些区是 FULL_FRAG 的?这时候XDES Entry中的 List Node 部分就派上用场了,我们可以通过List Node 中的指针,做这么三件事:

把状态为FREE的区对应的XDES Entry结构通过List Node来连接成一个链表,这个链表我们就称之为FREE 链表。

把状态为FREE_FRAG的区对应的XDES Entry结构通过List Node来连接成一个链表,这个链表我们就称之为FREE_FRAG链表。

把状态为FULL_FRAG的区对应的XDES Entry结构通过List Node来连接成一个链表,这个链表我们就 称之为FULL_FRAG 链表。

这样每当我们想找一个FREE_FRAG状态的区时,就直接把 FREE_FRAG链表的头节点拿出来,从这个节点中取一些零碎的页来插入数据,当这个节点对应的区用完时,就修改一下这个节点的State字段的值, 然后FREE_FRAG链表中移到FULL_FRAG链表中。同理,如果FREE_FRAG 链表中一个节点都没有,那么就直接从FREE 链表中取一个节点移动到FREE_FRAG 链表的状态,并修改该节点的STATE 字段值为FREE_FRAG ,然后从这个节点对应的区中获取零碎的页就好了。当段中数据已经占满了32个零散的页后,就直接申请完整的区来插入数据了。

那么我们怎么知道哪些区属于哪个段的呢?InnoDB为每个段中的区对应的XDES Entry结构建立了三个链表:

FREE链表:同一个段中,所有页面都是空闲的区对应的XDES Entry 结构会被加入到这个链表。注意和直属于表空间的FREE 链表区别开了,此处的FREE 链表是附属于某个段的。

NOT_FULL链表:同一个段中,仍有空闲空间的区对应的XDES Entry 结构会被加入到这个链表。

FULL链表:同一个段中,已经没有空闲空间的区对应的XDES Entry 结构会被加入到这个链表。

注意:每一个索引都对应两个段,每个段都会维护上述的3个链表,同时还会维护直属于表空间的3个链表,整个独立表空间共需要维护(索引数23+3)个链表。所以段在数据量比较大时插入数据的话,会先获取NOT_FULL链表的头节点,直接把数据插入这个头节点对应的区中即可,如果该区的空间已经被用完,就把该节点移到FULL链表中。

链表基节点

我们怎么找到这些链表呢?InnoDB 设计了一个叫List Base Node 的结构,这个结构中包含了链表的头节点和尾节点的指针以及这个链表中包含了多少节点的信息,如下图所示:

在这里插入图片描述

上边介绍的每个链表都对应一个List Base Node 结构,其中: List Length 表明该链表一共有多少节点。First Node Page Number 和 First Node Offset 表明该链表的头节点在表空间中的位置。 Last Node Page Number 和 Last Node Offset 表明该链表的尾节点在表空间中的位置。一般我们把某个链表对应的List Base Node 结构放置在表空间中固定的位置,这样我们就能轻易定位到某个链表了。

段的结构

上述内容讲到每个区都有对应的XDES Entry 来记录这个区中的属性一样,同样每个段都定义了一个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 。

Magic Number : 这个值是用来标记这个INODE Entry 是否已经被初始化了(初始化的意思就是把各个字段的值都填进去了)。

Fragment Array Entry: 每个Fragment Array Entry 结构都对应 着一个零散的页面,这个结构一共4个字节,表示一个零散页面的页号。

页面各类型页面详细情况

我们前边介绍了每256个连续的区算是一个组,下面我们介绍一下每个组开头的一些不同类型的页面

FSP_HDR 类型

首先看第一个组的第一个页面也就是表空间的第一个页面,页号是0,这个页面是FSP_HDR类型,它存储了表空间的一些整体属性以及第一个组内256个区的对应的XDES Entry结构。

在这里插入图片描述
一个完整的FSP_HDR 类型的页面大致由5个部分组成,我们重点来看看File Space Header 和 XDES Entry 这两个部分。

File Space Header 部分是用来存储表空间的一些整体属性的,如下图所示:
在这里插入图片描述

File Space Header结构各字段含义如下表所示:

名称占用空间描述
Space ID4字节表空间的ID
Not Used4字节未被使用
Size4字节当前表空间占有的页面数
FREE Limit4字节尚未被初始化的最小页号,大于或等于这个页号的区对应的XDES Entry结构都 没有被加入FREE链表
Space Flags4字节表空间的一些占用存储空间比较小的属性
FRAG_N_USED4字节FREE_FRAG链表中已使用的页面数量
List Base Node for FREE List16字节FREE链表的基节点
List Base Node for FREE_FRAG List16字节FREE_FREG链表的基节点
List Base Node for FULL_FRAG List16字节FULL_FREG链表的基节点
Next Unused Segment ID8字节当前表空间中下一个未使用的 Segment ID
List Base Node for SEG_INODES_FULL16字节SEG_INODES_FULL链表的基节点
List List Base Node for SEG_INODES_FREE List16字节SEG_INODES_FREE链表的基节点

下面我们再聊聊XDES Entry部分,XDES Entry就是在表空间的第一个页面中保存的。我们知道一个XDES Entry结构的大小是40字节,但是一个页面的大小有限,只能存放有限个XDES Entry结构,所以我们才把256个区划分成一组,在每组的第一个页面中存放 256个XDES Entry结构。每个XDES Entry就对应着一个extent。

XDES类型

我们说过,第一个组的第一个页面(FSP_HDR类型)除了记录本组中的所有区对应的XDES Entr 结构以外,还记录着表空间的一些整体属性,整个表空间里只有一个这个类型的页面。除去第一个分组以外,之后的每个分组的第一个页面只需要记录本组内所有的区对应的XDES Entry结构即可,不需要再记录表空间的属性了,我们把之后每个分组的第一个页面的类型定义为XDES,它的结构和FSP_HDR 类型是非常相似的。

IBUF_BITMAP类型

对比前边介绍表空间的图,每个分组的第二个页面的类型都是IBUF_BITMAP ,这种类型的页里边记录了一些有关Change Buffer的东东,今天先不讲这个,大家有个印象即可。

INODE类型

对比前边介绍表空间的图,第一个分组的第三个页面的类型是INODE。我们前边说过每个索引定义了两个段,而且为某些特殊功能定义了些特殊的段。为了方便管理,InnoDB为每个段设计了一个INODE Entry 结构,这个结构中记录了关于这个段的相关属性。
在这里插入图片描述
一个INODE 类型的页面是由5部分构成的,我们重点关注 List Node for INODE Page List 和 INODE Entry 这两个部分。

INODE Entry部分主要包括对应的段内零散页面的地址以及附属于该段的FREE 、NOT_FULL 和 FULL 链表的基节点。每个 INODE Entry 结构占用192字节,一个页面里可以存储85个这样的结构。

重点看一下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 里边,也就是说这两个链表的基节点的位置是固定的,所以我们可以很轻松的访问到这两个链表。以后每当我们新创建一个段时,都会创建一个INODE Entry结构与之对应,存储 INODE Entry 的大致过程就是这样的:

先看看SEG_INODES_FREE链表是否为空,如果不为空,直接从该链表中获取一个节点,也就相当于获取到一 个仍有空闲空间的INODE 类型的页面,然后把该INODE Entry结构放到该页面中。当该页面中无剩余空间时,就把该页放到SEG_INODES_FULL 链表中。如果SEG_INODES_FREE链表为空,则需要从表空间的FREE_FRAG链表中申请一个页面,修改该页面的类型为INODE,把该页面放到 SEG_INODES_FREE链表中,与此同时把该INODE Entry结构放入该页面。

Segment Header 结构的运用

我们知道一个索引会产生两个段,分别是叶子节点段和非叶子节点段,而每个段都会对应一个INODE Entry结构,那我们怎么知道某个段对应哪个INODE Entry 结构呢?

INDEX 类型的页也就是数据页有一个Page Header 部分,这个部分包含了两个属性:PAGE_BTR_SEG_LEAF 和 PAGE_BTR_SEG_TOP 都占用10个字节,它们其实对应一个叫 Segment Header 的结构,该结构图示如下:
在这里插入图片描述

各个部分的具体释义如下:

Space ID of the INODE Entry: INODE Entry结构所在的表空间ID。
Page Number of the INODE Entry: INODE Entry结构所在的页面页号。
Byte Offset of the INODE Ent: INODE Entry结构在该页面中的偏移量 。

这样子就很清晰了,PAGE_BTR_SEG_LEAF记录着叶子节点段对应的 INODE Entry结构的地址是哪个表空间的哪个页面的哪个偏移量,PAGE_BTR_SEG_TOP记录着非叶子节点段对应的INODE Entry结构的地址是哪个表空间的哪个页面的哪个偏移量。这样子索引和其对应的段的关系就建立起来了。不过需要注意的一点是,因为一个索引只对应两个段,所以只需要在索引的根页面中记录这两个结构即可。

今天讲的内容确实太绕了,我在这里给大家总结了一张图,大家可以根据这张图再回顾一下讲的内容。
在这里插入图片描述
有想要原图的可以私信联系我,我发给大家。好了,今天的内容就先讲到这里,内容太多,大家可以好好消化一下,有什么疑问欢迎在评论区进行讨论。最后依旧是请各位老板有钱的捧个人场,没钱的也捧个人场,谢谢各位老板!

  • 28
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

韩朝洋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值