mysql 学习 - InnoDB的表空间
本篇已收录在 MySQL 是怎样运行的 学习笔记系列
表空间是一个抽象的概念,对于系统表空间来说,对应着文件系统中一个或多个实际文件;对于每个独立表空间来说,对应着文件系统中一个名为表名.ibd的实际文件。大家可以把表空间想象成被切分为许许多多个页的池子,当我们想为某个表插入一条记录的时候,就从池子中捞出一个对应的页来把数据写进去。
注: 要完整的理解表空间的所有结构真的非常困难....
独立表空间结构
我们知道InnoDB支持许多种类型的表空间,本篇重点关注独立表空间和系统表空间的结构。它们的结构比较相似,但是由于系统表空间中额外包含了一些关于整个系统的信息,所以先介绍简单一点的独立表空间,
稍后再说系统表空间的结构
区(extent)的概念
表空间中的页实在是太多了,为了更好的管理这些页面,InnoDB提出了区(英文名:extent)的概念。对于16KB的页来说,连续的64个页就是一个区
,也就是说一个区默认占用1MB空间大小。不论是系统表空间还是独立表空间,都可以看成是由若干个区组成的,每256个区被划分成一组
。画个图表示就是这样:
段(segment)的概念
在了解段的概念之前, 先问问为什么引入区的概念. mysql 引入区的概念主要是为了解决随机IO的问题. 因为我们知道页与页之间是通过双向链表连接起来的. 所以两个页之间即使主键是连续的, 但是在物理地址上可能相距很远. 但是磁盘访问数据时, 要根据链表的顺序去访问, 那么这种访问相距很远的'连续的'两个页, 就相当于发生了随机IO. mysql 为了减少随机IO的发生, 于是提出了区的概念. 一个区就是在物理地址上连续的64个页. 当表的数据量特别大的时候, 为某个索引分配空间的时候就按照区去分, 甚至一次分配多个区, 为了保证页之间的物理地址的连续性.
那么上述内容就能很好的解决随机IO的发生吗. 并不是. 如果只有区的概念, 那么相当于所有的叶子节点(数据记录)和非叶子节点(索引页目录记录)都是连续分配在区中的. 此时如果执行一次范围查询, 还是会在区中发生多次随机IO.
此时为了解决区中的随机IO的发生频率, mysql提出了段的概念. 所有的叶子节点放到一个独有的区中, 所有存放叶子节点的区就被成为段. 对应的所有存放了非叶子节点的区也是一个段. 也就是说一个聚簇索引中会产生两个段, 叶子节点段与非叶子节点段.
默认情况下一个使用InnoDB存储引擎的表只有一个聚簇索引,一个索引会生成2个段,而段是以区为单位申请存储空间的,一个区默认占用1M存储空间,所以默认情况下一个只存了几条记录的小表也需要2M的存储空间么?以后每次添加一个索引都要多申请2M的存储空间么?这对于存储记录比较少的表简直是天大的浪费。这个问题的症结在于到现在为止我们介绍的区都是非常纯粹的,也就是一个区被整个分配给某一个段,或者说区中的所有页面都是为了存储同一个段的数据而存在的,即使段的数据填不满区中所有的页面,那余下的页面也不能挪作他用
。现在为了考虑以完整的区为单位分配给某个段对于数据量较小的表太浪费存储空间的这种情况,mysql 提出了一个碎片(fragment)区
的概念,也就是在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,而是碎片区中的页可以用于不同的目的,比如有些页用于段A,有些页用于段B,有些页甚至哪个段都不属于。碎片区直属于表空间,并不属于任何一个段。
所以此后为某个段分配存储空间的策略是这样的:
在刚开始向表中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的。
当某个段已经占用了32个碎片区页面之后,就会以完整的区为单位来分配存储空间。
段虽然是一个抽象的概念, 但是mysql还是提供了一个叫做INODE Entry
的结构用于存储段的属性:
区的分类
空闲的区:现在还没有用到这个区中的任何页面。
有剩余空间的碎片区:表示碎片区中还有可用的页面。
没有剩余空间的碎片区:表示碎片区中的所有页面都被使用,没有空闲页面。
附属于某个段的区。每一个索引都可以分为叶子节点段和非叶子节点段,除此之外InnoDB还会另外定义一些特殊作用的段,在这些段中的数据量很大时将使用区来作为基本的分配单位。
梳理一下向表中插入数据的一次过程
我们把事情搞这么麻烦的初心仅仅是想提高向表插入数据的效率又不至于数据量少的表浪费空间。现在我们知道向表中插入数据本质上就是向表中各个索引的叶子节点段、非叶子节点段插入数据,也知道了不同的区有不同的状态,再回到最初的起点,捋一捋向某个段中插入数据的过程:
当段中数据较少的时候,首先会查看表空间中是否有状态为FREE_FRAG的区,也就是找还有空闲空间的碎片区,如果找到了,那么从该区中取一些零散的页把数据插进去;否则到表空间下申请一个状态为FREE的区,也就是空闲的区,把该区的状态变为FREE_FRAG,然后从该新申请的区中取一些零散的页把数据插进去。之后不同的段使用零散页的时候都会从该区中取,直到该区中没有空闲空间,然后该区的状态就变成了FULL_FRAG。
当段中数据已经占满了32个零散的页后,就直接申请完整的区来插入数据了。
XDES Entry 链表
每一个区都对应着一个XDES Entry结构,这个结构记录了对应的区的一些属性。
结构名 | 描述 |
---|---|
Segment ID(8字节) | 每一个段都有一个唯一的编号,用ID表示,此处的Segment ID字段表示就是该区所在的段。当然前提是该区已经被分配给某个段了,不然的话该字段的值没啥意义。 |
List Node(12字节) | 这个部分可以将若干个XDES Entry结构串联成一个链表,大家看一下这个List Node的结构: |
State(4字节) | 这个字段表明区的状态。可选的值就是我们前边说过的那4个,分别是:FREE、FREE_FRAG、FULL_FRAG和FSEG。 |
Page State Bitmap(16字节) | 这个部分共占用16个字节,也就是128个比特位。我们说一个区默认有64个页,这128个比特位被划分为64个部分,每个部分2个比特位,对应区中的一个页. |
List Node 的作用非常大, 可以将不同的状态的区串联起来:
把状态为FREE的区对应的XDES Entry结构通过List Node来连接成一个链表,这个链表我们就称之为FREE链表。
把状态为FREE_FRAG的区对应的XDES Entry结构通过List Node来连接成一个链表,这个链表我们就称之为FREE_FRAG链表。
把状态为FULL_FRAG的区对应的XDES Entry结构通过List Node来连接成一个链表,这个链表我们就称之为FULL_FRAG链表。
这样做的好处就是不需要通过遍历找一个指定状态的区了. 那么如何能够找到这些链表在表空间中的头位置或者尾位置呢? 此时 mysql 还提出了 List Base Node 的概念帮助你找到链表头部.
链表基节点 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结构放置在表空间中固定的位置,这样想找定位某个链表就容易多了.
表空间梳理
现在我们已经知道了表空间中大多数数据结构的概念以及作用了. 其实表空间中除了有区的概念, 还对区进行了分组, 每 256 个连续的区被分为一组, 每个组的排在最前面的两页或者三页的类型是固定的:
FSP_HDR类型
:
首先看第一个组的第一个页面,当然也是表空间的第一个页面,页号为0。
这个页面的类型是FSP_HDR.
它存储了表空间的一些整体属性以及第一个组内256个区的对应的XDES Entry结构.
直接看这个类型的页面的示意图:
XDES类型
:
由于第一个组的第一个页面有些特殊,因为它也是整个表空间的第一个页面,所以除了记录本组中的所有区对应的XDES Entry结构以外,还记录着表空间的一些整体属性,这个页面的类型就是我们刚刚说完的FSP_HDR类型,整个表空间里只有一个这个类型的页面。除去第一个分组以外,之后的每个分组的第一个页面只需要记录本组内所有的区对应的XDES Entry结构即可,不需要再记录表空间的属性了,为了和FSP_HDR类型做区别,我们把之后每个分组的第一个页面的类型定义为XDES,它的结构和FSP_HDR类型是非常相似的:
INODE类型
:
第一个分组的第三个页面的类型是INODE.
我们前边说过 mysql 为每个索引定义了两个段,而且为某些特殊功能定义了些特殊的段。为了方便管理,他们又为每个段设计了一个INODE Entry
结构,这个结构中记录了关于这个段的相关属性。而我们这会儿要介绍的这个 INODE类型的页 就是为了存储INODE Entry结构而存在的
。
小结
是的, 没有介绍系统表空间结构...上面的概念已经很多了...