MySQL是怎样运行的:(9)InnoDB的表空间
Session 9 InnoDB的表空间
本章概括:
本章会总览==页的类型==。为了方便,称呼类型时省略前缀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
表示页的类型)。本章要求一定能看懂下图:
段(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 |
附属于某个段的区 |
处于FREE
、FREE_FRAG
以及FULL_FRAG
这三种状态的区都是独立的,算是直属于表空间。
为了方便管理这些区,设计InnoDB
的大佬设计了一个称为XDES Entry
的结构(全称就是Extent Descriptor Entry),每一个区都对应着一个XDES Entry
结构(==entry:条目==),这个结构记录了对应的区的一些属性。
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
结构(具体储存在哪,下文会说)。
段的结构
段其实不对应表空间中某一个连续的物理区域,而是一个逻辑上的概念,由若干个零散的页以及一些完整的区组成。像每个区都有对应的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
。so easy! Magic Number
:这个值是用来标记这个INODE Entry
是否已经被初始化了(初始化的意思就是把各个字段的值都填进去了)。Fragment Array Entry
:我们前面强调过无数次:段是一些零散页和一些完整的区的集合,每个Fragment Array Entry
结构都对应着一个零散的页,这个结构一共4个字节,表示一个零散页的页号。
第一个组的第一个页,也就是表空间的第一个页,页号为0
。这个页的类型是FSP_HDR(表空间头),它存储了表空间的一些整体属性以及第一个组内256个区的对应的XDES Entry
结构。
可以看到,List Base Node for FREE List
、List Base Node for FREE_FRAG List
、List Base Node for FULL_FRAG List
(也就是直属于表空间的FREE
链表的基节点、FREE_FRAG
链表的基节点、FULL_FRAG
链表的基节点)就储存在表空间的第一个页中。
List Base Node for SEG_INODES_FULL List
和List 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
结构。
与FSP_HDR
类型的页对比,除了少了File Space Header
部分之外,也就是除了少了记录表空间整体属性的部分之外,其余的部分是一样一样的。
INODE
类型的页就是为了存储INODE Entry
结构而存在的。
重点看一下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
的结构,该结构图示如下:
名称 | 占用字节数 | 描述 |
---|---|---|
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
。
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_TABLES
、SYS_COLUMNS
、SYS_INDEXES
、SYS_FIELDS
这四个表尤其重要,称之为基本系统表(basic system tables)。
剩余部分暂不深入,除非后续涉及