Session 9 InnoDB的表空间

本章概括:

image-20230727133025281

本章会总览==页的类型==。为了方便,称呼类型时省略前缀FIL_PAGE_TYPE_

类型名称 十六进制 描述
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 索引页,也就是我们所说的数据页

先从独立表空间进行介绍。

表空间分为若干个组,每个组含有256个**(英文名:extent,区在空间上是连续的。每个区包含了连续64个页。因此对于16KB的页来说,一个区默认占用1MB空间大小。整个表空间可容纳2^32个页(页号不会超过4字节**)。

表空间(的第一个组的第一个区)最开始的3个页的类型是固定的。(页的File Header部分中,属性FIL_PAGE_TYPE表示页的类型)。本章要求一定能看懂下图:

image-20230727135247415

段(segment)

为什么要引入区的概念?

我们每向表中插入一条记录,本质上就是向该表的聚簇索引以及所有二级索引代表的B+树的节点中插入数据。而B+树的每一层中的页都会形成一个双向链表,如果是以为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远。我们介绍B+树索引的适用场景的时候特别提到范围查询只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中相邻的两个页物理位置离得非常远,就变成了所谓的随机I/O。再一次强调,磁盘的速度和内存的速度差了好几个数量级,随机I/O是非常慢的,所以我们应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以使用所谓的顺序I/O

这样,在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照为单位分配,甚至在表中的数据十分非常特别多的时候,可以一次性分配多个连续的区。

叶子节点有自己独有的,非叶子节点也有自己独有的。存放叶子节点的区的集合就算是一个segment),存放非叶子节点的区的集合也算是一个。也就是说一个索引会生成2个段,一个叶子节点段,一个非叶子节点段。

碎片(fragment)区

为了不让几条数据就占用1M的区,引入碎片区的概念。在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,而是碎片区中的页可以用于不同的目的,比如有些页用于段A,有些页用于段B,有些页甚至哪个段都不属于。碎片区直属于表空间,并不属于任何一个段。所以此后为某个段分配存储空间的策略是这样的:

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

区的4种状态(State

状态名 含义
FREE 空闲的区
FREE_FRAG 有剩余空间的碎片区
FULL_FRAG 没有剩余空间的碎片区
FSEG 附属于某个段的区

处于FREEFREE_FRAG以及FULL_FRAG这三种状态的区都是独立的,算是直属于表空间。

为了方便管理这些区,设计InnoDB的大佬设计了一个称为XDES Entry的结构(全称就是Extent Descriptor Entry),每一个区都对应着一个XDES Entry结构(==entry:条目==),这个结构记录了对应的区的一些属性。

image-20230727140854389

Segment ID字段表示就是该区所在的段。当然前提是该区已经被分配给某个段了。

List Node(12字节)部分可以将若干个XDES Entry结构串联成一个双向链表。如果我们想定位表空间内的某一个位置的话,只需指定页号(4字节)以及该位置在指定页号中的页内偏移量(2字节)即可。

State字段表明区的状态。即前面说的那四个。

XDES Entry链表

我们不能遍历地去找哪些区是FREE的,哪些区是FREE_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,然后从这个节点对应的区中获取零碎的页就好了。

每个段都有应该它独立的链表,可以根据段号(也就是Segment ID)来建立。因为一个段中可以有好多个区,有的区是完全空闲的,有的区还有一些页可以用,有的区已经没有空闲页可以用了,所以我们有必要继续细分。设计InnoDB的大佬们为每个段中的区对应的XDES Entry结构建立了三个链表:

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

我们创建一张innoDB表,该表至少有一个聚簇索引,就至少有6个链表。

链表基节点

List Base Node(链表的基节点)结构包含了链表的头节点和尾节点的指针以及这个链表中包含了多少节点的信息。任何一个列表都一定对应了一个List Base Node结构(具体储存在哪,下文会说)。

image-20230727142911721

段的结构

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

image-20230727143154818

  • 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是否已经被初始化了(初始化的意思就是把各个字段的值都填进去了)。
  • Fragment Array Entry:我们前面强调过无数次:段是一些零散页和一些完整的区的集合,每个Fragment Array Entry结构都对应着一个零散的页,这个结构一共4个字节,表示一个零散页的页号。

FSP_HDR类型

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

image-20230727144203281

image-20230727144246851

可以看到,List Base Node for FREE ListList Base Node for FREE_FRAG ListList Base Node for FULL_FRAG List(也就是直属于表空间的FREE链表的基节点、FREE_FRAG链表的基节点、FULL_FRAG链表的基节点)就储存在表空间的第一个页中。

  • List Base Node for SEG_INODES_FULL ListList Base Node for SEG_INODES_FREE List:每个段对应的INODE Entry结构会集中存放到一个类型位INODE的页中,如果表空间中的段特别多,则会有多个INODE Entry结构,可能一个页放不下,这些INODE类型的页会组成两种列表:
    • SEG_INODES_FULL链表,该链表中的INODE类型的页都已经被INODE Entry结构填充满了,没空闲空间存放额外的INODE Entry了。
    • SEG_INODES_FREE链表,该链表中的INODE类型的页都已经仍有空闲空间来存放INODE Entry结构。

XDES类型

FSP_HDR类型的页对比,除了少了File Space Header部分之外,也就是除了少了记录表空间整体属性的部分之外,其余的部分是一样一样的。

image-20230727144111100

INODE类型

INODE类型的页就是为了存储INODE Entry结构而存在的。

image-20230727145217644

重点看一下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结构了。

我们怎么知道某个段对应哪个INODE Entry结构呢?回忆一下,INDEX类型的页有一个Page Header部分,其中(为突出重点,省略了其他属性):

名称 占用空间大小 描述
PAGE_BTR_SEG_LEAF 10字节 B+树叶子段的头部信息,仅在B+树的根页定义
PAGE_BTR_SEG_TOP 10字节 B+树非叶子段的头部信息,仅在B+树的根页定义

它们其实对应一个叫Segment Header的结构,该结构图示如下:

image-20230727152723909
名称 占用字节数 描述
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结构在该页中的偏移量

系统表空间

系统表空间的结构和独立表空间基本类似,只不过由于整个MySQL进程只有一个系统表空间,在系统表空间中会额外记录一些有关整个系统信息的页,所以会比独立表空间多出一些记录这些信息的页。表空间 ID(Space ID)为0

image-20230729220749573

MySQL除了保存着我们插入的用户数据之外,还需要保存许多额外的信息,比方说:

  • 某个表属于哪个表空间,表里边有多少列
  • 表对应的每一个列的类型是什么
  • 该表有多少索引,每个索引对应哪几个字段,该索引对应的根页在哪个表空间的哪个页
  • 该表有哪些外键,外键对应哪个表的哪些列
  • 某个表空间对应文件系统上文件路径是什么

 上述这些数据并不是我们使用INSERT语句插入的用户数据,实际上是为了更好的管理我们这些用户数据而不得已引入的一些额外数据,这些数据也称为元数据。InnoDB存储引擎特意定义了一些列的内部系统表(internal system table)来记录这些这些元数据

表名 描述
SYS_TABLES 整个InnoDB存储引擎中所有的表的信息
SYS_COLUMNS 整个InnoDB存储引擎中所有的列的信息
SYS_INDEXES 整个InnoDB存储引擎中所有的索引的信息
SYS_FIELDS 整个InnoDB存储引擎中所有的索引对应的列的信息
SYS_FOREIGN 整个InnoDB存储引擎中所有的外键的信息
SYS_FOREIGN_COLS 整个InnoDB存储引擎中所有的外键对应列的信息
SYS_TABLESPACES 整个InnoDB存储引擎中所有的表空间信息
SYS_DATAFILES 整个InnoDB存储引擎中所有的表空间对应文件系统的文件路径信息
SYS_VIRTUAL 整个InnoDB存储引擎中所有的虚拟生成列的信息

  这些系统表也被称为==数据字典==,它们都是以B+树的形式保存在系统表空间的某些页中,其中SYS_TABLESSYS_COLUMNSSYS_INDEXESSYS_FIELDS这四个表尤其重要,称之为基本系统表(basic system tables)。

剩余部分暂不深入,除非后续涉及