表空间是一个抽象的概念,对于系统表空间来说,对应着文件系统中一个或多个实际文件;对于每个独立表空间来说,对应着文件系统中一个名为 表名.ibd 的实际文件

1. 页面类型

InnoDB 是以页为单位管理存储空间的,我们的聚簇索引(也就是完整的表数据)和其他的二级索引都是以B+树的形式保存到表空间的,而B+树的节点就是数据页。我们前边说过,这个数据页的类型名其实是:FIL_PAGE_INDEX,除了这种存放索引数据的页面类型之外,InnoDB 也为了不同的目的设计了若干种不同类型的页面

截屏2021-05-18 下午10.23.42

2. 独立表空间结构

InnoDB 支持许多种类型的表空间,重点关注独立表空间和系统表空间的结构

2.1. extent

表空间为了更好的管理页面,InnoDB 提出了(extent)的概念。对于 16KB 的页来说,连续的64个页就是一个,也就是说一个区默认占用 1MB 空间大小。不论是系统表空间还是独立表空间,都可以看成是由若干个区组成的,每256个区被划分成一组。

截屏2021-05-18 下午11.02.46
  • 第一个组最开始的由3个页面的类型是固定的,也就是说extent 0这个区最开始的3个页面的类型是固定的,分别是:

    • FIL_PAGE_TYPE_FSP_HDR 类型: 页面是用来登记整个表空间的一些整体属性以及本组所有的,也就是extent 0 ~ extent 255 这256个区的属性,整个表空间只有一个FSP_HDR类型的页面。

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

    • FIL_PAGE_INODE 类型:这个类型的页面存储了许多称为INODE的数据结构

  • 其余各组最开始的 2 个页面的类型是固定的,也就是说 extent 256extent 512 这些区最开始的2个页面的类型是固定的,分别是:

    • XDES 类型: 全称是 extent descriptor,用来登记本组256个区的属性,也就是说对于在extent 256区中的该类型页面存储的就是extent 256 ~ extent 511这些区的属性,对于在extent 512区中的该类型页面存储的就是extent 512 ~ extent 767这些区的属性。上边介绍的FSP_HDR类型的页面其实和XDES类型的页面的作用类似,只不过FSP_HDR类型的页面还会额外存储一些表空间的属性。
    • IBUF_BITMAP类型

截屏2021-05-18 下午10.45.41

2.2. segment

一个区就是在物理位置上连续的64个页。在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照为单位分配,甚至在表中的数据十分非常特别多的时候,可以一次性分配多个连续的区。虽然可能造成一点点空间的浪费(数据不足填充满整个区),但是从性能角度看,可以消除很多的随机I/O

2.2.1. 叶子节点段&非叶子节点段

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

2.2.2. 碎片(fragment)区

默认情况下一个使用InnoDB存储引擎的表只有一个聚簇索引,一个索引会生成2个段,而段是以区为单位申请存储空间的,一个区默认占用1M存储空间,所以默认情况下一个只存了几条记录的小表也需要2M的存储空间么?以后每次添加一个索引都要多申请2M的存储空间么?这对于存储记录比较少的表造成很大的浪费

InnoDB 提出了一个碎片(fragment)区的概念,在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,而是碎片区中的页可以用于不同的目的,比如有些页用于段A,有些页用于段B,有些页甚至哪个段都不属于。碎片区直属于表空间,并不属于任何一个段。所以此后为某个段分配存储空间的策略是这样的:

  • 在刚开始向表中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的。
  • 当某个段已经占用了32个碎片区页面之后,就会以完整的区为单位来分配存储空间。

所以现在段不能仅定义为是某些区的集合,更精确的应该是某些零散的页面以及一些完整的区的集合。除了索引的叶子节点段和非叶子节点段之外,InnoDB中还有为存储一些特殊的数据而定义的段,比如回滚段,当然我们现在并不关心别的类型的段,现在只需要知道段是一些零散的页面以及一些完整的区的集合就好了。

为了方便管理这些区,InnoDB设计了一个称为 XDES Entry 的结构,每一个区都对应着一个 XDES Entry 结构,这个结构记录了对应的区的一些属性

截屏2021-05-19 上午8.09.52

XDES Entry 是一个 40 个字节的结构,大致分为 4 个部分

截屏2021-05-19 上午8.32.35

3. XDES Entry 链表

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

  • 把状态为 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结构会被加入到这个链表。

4. 链表基节点

List Base Node 结构中包含了链表的头节点和尾节点的指针以及这个链表中包含了多少节点的信息

截屏2021-05-19 上午9.02.21

一般把某个链表对应的 List Base Node 结构放置在表空间中固定的位置,这样想找定位某个链表就变得so easy啦

5. 段的结构

段不对应表空间中某一个连续的物理区域,而是一个逻辑上的概念,由若干个零散的页面以及一些完整的区组成。像每个区都有对应的 XDES Entry 来记录这个区中的属性一样,InnoDB 为每个段都定义了一个 INODE Entry 结构来记录一下段中的属性

截屏2021-05-19 上午9.20.21

截屏2021-05-19 上午9.30.56