InnoDB 表空间
独立表空间结构
区(extent)
通常,表空间中的页会非常非常多,为了更好地管理这些页,InnoDB 引入了区(extent)的概念。规定一个区由连续的 64 个页组成,如果默认使用 16 KB 大小的页,那么一个区大小为 1 MB。此外,每 256 个区还会划分成一个组,大概如图所示:
从图中我们可以看出,第一组的前 3 个页类型是固定的,分别是:
FSP_HDR:存储当前表空间的一些信息,以及该组所有区的信息
IBUF_BITMAP:存储该组所有区的所有页关于 INSERT BUFFER 的信息
INODE:存储段的相关信息
除了第一组,后面组的前 2 个页类型是固定的,分别是:
XDES:全称是 extent descriptor,类似 FSP_HDR,区别在于 XDES 只存储该组所有区的信息,而 FSP_HDR 还额外存储了当前表空间的一些信息
IBUF_BITMAP:存储该组所有区的所有页关于 INSERT BUFFER 的信息
InnoDB 为什么要引入区呢?
其实主要是为了规避机械硬盘随机IO性能低下的问题。我们知道使用索引进行范围查询的时候,只需要找到符合条件的最左边的记录和最右边的记录,然后沿着双向链表上的页一一扫描即可。由于是链表结构,相邻的页在物理磁盘上可能相隔很远,这时候沿着链表扫描页就是随机IO操作,性能比较差劲。为了将随机IO尽可能转化成顺序IO,InnoDB 就引入了区,一个区是物理磁盘上连续的 64 个页,当表中数据量大的时候,InnoDB 不再按页为索引分配空间,会改成按区分配,数据里足够多的时候甚至会连续分配多个连续的区,这样使用索引范围查询的时候就可以尽量接近顺序IO的性能。
段(segment)
除了区,InnoDB 还引入了段(segment)来进一步优化大数据量下使用索引范围查找的性能。段是一个逻辑概念,并不对应表空间中某一块连续的物理区域。我们知道 B+ 树分叶子节点和非叶子节点,在没有段的情况下,叶子页和非叶子页在物理磁盘上的排列是混在一起的,这势必会影响顺序查找叶子节点的性能。因此,InnoDB 引入了段,叶子节点和非叶子节点都有自己独有的区,存放叶子节点的区的集合就划分成一个段(数据段),存放叶子节点的区的集合划分成另一个段(索引段),这样顺序查找叶子节点就更接近顺序IO了。
除了索引段和数据段,还有其他的段,比如:回滚段。
碎片区
每张表都至少有一个索引,一个索引有2个段,一个段至少占用 1MB 的空间,这样算下来一张表至少占用 2MB 的存储空间。但是实际情况中会存在一些数据量比较少的表,InnoDB 不想浪费这 2MB 空间,引入了碎片区(fragment)的概念。碎片区中的页可以用于不同的段,还可以不属于任何段。对于数据量很少的表,InnoDB 使用碎片页来存储数据,节省了磁盘容量开销。
因此,给段分配存储空间的策略如下:
当表中数据量很少的时候,从碎片区以页为单位给段分配存储空间
当某个段已经使用了碎片区中的 32 个页后,就会改成以完整的区为单位给段分配存储空间
知道了碎片区的概念,此时我们可以给出段的准确定义:段其实是一些完整区和一些碎片页的集合。
区的分类
区大致分成以下 4 类:
InnoDB 为了管理区以及区中页的状态,设计了一个 XDES Entry 结构,每个区都有一个对应的 XDES Entry 存储其状态信息。在 FSP_HDR 页 和 XDES 页中都有存储 XDES Entry,并且这两种页分别都存储了 256 个XDES Entry,对应所在组的 256 个区。
我们来仔细看下 XDES Entry 的结构:
从图中可以看出,XDES Entry 是一个 40 字节的结构,分为 4 个部分:
Segment ID:段id,表示该区所属的段
List Node:用于将若干个 XDES Entry 组成一个链表
State:区状态,有 4 种取值:FREE、FSG、FREE_FRAG、FULL_FRAG
Page State Bitmap:这个部分占用 16 字节,即 128 比特位,我们知道一个区有 64 个页,这 128 个比特位被划分为 64 个部分,每两个比特位对应一个页。在这两个比特位中,第一个比特位用于表示对应页是否空闲,第二个比特位暂未使用。
为什么要将区分组呢?
一个 XDES Entry 结构占40字节,算下来一个页大概可以存放几百个 XDES Entry,但是表空间中区的数量通常远不止几百个,一个页无法存储所有的 XDES Entry,因此 InnoDB 把表空间中的区分成了若干个组,一个组包含 256 个区,在每个组的第一个页面存储本组的所有区对应的 256 个 XDES Entry 结构。
为什么要将 XDES Entry 组成链表呢?
我们先梳理下插入数据的过程:
当段中数据较少的时候,首先查看表空间中是否还有状态为 FREE_FRAG 的区(有剩余空间的碎片区),如果还有,则从中取一些碎片页插入数据;如果没有,则从表空间申请一个状态位 FREE 的区,将其状态改为 FREE_FRAG,然后从中取一些碎片页插入数据。当 FREE_FRAG 区中的页都用完时,其状态会变为 FULL_FRAG。
当段已经使用了 32 个碎片页之后,就直接申请完整的区来插入数据。
现在有几个问题:
如何找到不同状态的区?
遍历是最直接的方式,但是当表中数据量很大的时候遍历的效率会很低(1GB 的表的区数量大于 1000 个)。因此,考虑到性能,InnoDB 选择使用 XDES Entry 中的 List Node 构建几个链表:
FREE 链表:把状态为 FREE 的区对应的 XDES Entry 连接起来
FREE_FRAG 链表:把状态为 FREE_FRAG 的区对应的 XDES Entry 连接起来
FULL_FRAG 链表:把状态为 FULL_FRAG 的区对应的 XDES Entry 连接起来
有了这 3 个链表,就能轻松拿到我们想要的区,如我们想找的 FREE_FRAG 状态的区,只需要把 FREE_FRAG 链表的头节点拿出来就行,其他状态同理。
怎么知道哪些区属于哪个段呢?
思路同上,InnoDB 给每个段都构建了 3 个链表,这些链表上的 XDES Entry 对应的区都属于这个段:
FREE 链表:所有页面都是空闲的区对应的 XDES Entry 会放入这个链表
NOT_FULL 链表:部分页面都是空闲的区对应的 XDES Entry 会放入这个链表
FULL 链表:没有空闲页面的区对应的 XDES Entry 会放入这个链表
注意:属于段的这些区状态为:FSG
怎么找到这些 XDES Entry 链表的头节点或者尾节点呢?
针对这个问题,InnoDB 设计了一个叫 List Base Node 的结构,其中存储了链表头尾节点的位置:
List Length:表示链表一共有多少个节点(每个节点代表一个区)
First Node Page Number 和 First Node Offset 用来定位链表头节点在表空间中的位置
Last Node Page Number 和 Last Node Offset 用来定位链表尾节点在表空间中的位置
InnoDB 把这些链表对应的 List Node Base 存储在了表空间中固定的位置,因此可以很方便地通过这些 List Base Node 定位想要找的链表。
段的结构
就像每个区都有对应的 XDES Entry 结构存储这个区中的信息一样,InnoDB 也给每个段设计了一个 INODE Entry 结构来存储这个段中的信息:
Segment ID:表示这个 INODE Entry 对应的段id
NOT_FULL_N_USED:表示 NOT_FULL 链表中使用了多少个页,用于下次从 NOT_FULL 链表中快速定位可分配的空闲页
List Base Node:段的 FREE、NOT_FULL、FULL 这 3 个链表的 List Base Node,记录了这些链表的头尾节点和节点数
Magic Number : 标记这个 INODE Entry 是否已经被初始化,如果值为 97937874 ,则表明该 INODE Entry 已经初始化,否则没有被初始化。 (初始化指的是填充各个字段的值)
Fragment Array Entry:每个 Fragment Array Entry 都对应一个碎片页,每个段可以有 32 个碎片页(Fragment Array Entry 中存储的是页号)
各种类型页的结构
FSP_HDR 页
FSP_HDR 是表空间的第一个页,存储了当前表空间以及该组所有区的信息:
File Space Header:表空间头部,存储表空间的一些整体信息
XDES Entry:存储对应区的信息(本组有 256 个)
Empty Space:用于填充页结构
File Space Header
Space ID:当前表空间id
Not Used:未使用的空间
Size:当前表空间拥有的页数
Free Limit:尚未被初始化的最小页号,该值之后的区对应的 XDES Entry 都没有加入 FREE 链表,即之后的区尚未被初始化,用来辅助懒初始化,目的是节省空间开销
Space Flags:表空间的一些属性,后面在详细介绍
FRAG_N_USED:FREE_FRAG 链表中已经使用的页数,用来快速定位空闲的页
LIst Base Node for FREE List:FREE 链表的头尾节点和节点数信息
LIst Base Node for FREE_FRAG List:FREE_FRAG 链表的头尾节点和节点数信息
LIst Base Node for FULL_FRAG List:FULL_FRAG 链表的头尾节点和节点数信息
Next Unused Segment ID:当前表空间下一个未使用的段id,创建新段的时候直接使用该值作为段id
LIst Base Node for SEG_INODE_FULL:SEG_INODES_FULL链表的头尾节点和节点数信息
LIst Base Node for SEG_INODE_FREE:SEG_INODES_FREE链表的头尾节点和节点数信息
如果表空间中的段特别多,一个 INODE 的页可能放不下,因此 InnoDB 设计了 LIst Base Node for SEG_INODE_FULL 和 LIst Base Node for SEG_INODE_FREE 两个链表来区分放满 INODE Entry 的页和未放满 INODE Entry 的页。
Space Flags 在不同版本可能有出入,下面是 5.7 版本的:
XDES 页
XDES 是表空间中每组(第一个组除外)第一个页,跟 FSP_HDR 页非常类似,不再赘述。
IBUF_BITMAP 页
IBUF_BITMAP 页存储了一些有关 Change Buffer 的信息。
INODE 页
我们已经介绍过 INODE Entry,而 INODE 页就是用来存储 INODE Entry 的:
List Node for INODE Page List:用于构成 SEG_INODES_FULL 和 SEG_INODES_FREE 这两个 INODE 页链表
INODE Entry:用于存储段相关信息
Empty Space:用于填充页结构
SEG_INODES_FULL 和 SEG_INODES_FREE 这两个链表的基节点存储在 FSP_HDR 页的 File Space Header 中。
Segment Header 结构
我们知道一个索引会产生两个段,分别是叶子节点段(数据段)和非叶子节点段(索引段),而每个段都会对应一个 INODE Entry 结构,那我们怎么知道某个段对应哪个 INODE Entry 结构呢?
InnoDB 把段和 INODE Entry 的对应关系保存在 INDEX 根页的 Page Header 中:
Page Header of Root INDEX:
PAGE_BTR_SEG_LEAF:叶子段的 Segment Header(仅在 B+ 树根页中定义)
PAGE_BTR_SEG_TOP:非叶子段的 Segment Header(仅在 B+ 树根页中定义)
Segment Header:
Space ID of the INODE Entry:INODE Entry 所在的表空间id
Page Number of the INODE Entry:INODE Entry 所在的页号
Byte Offset of the INODE Entry:INODE Entry 在页中的偏移量
综合上面对 InnoDB 独立表空间每个部分的介绍,我们可以画出它的完整结构:
系统表空间结构
系统表空间和独立表空间结构很类似,主要区别是系统表空间额外存储了整个 InnoDB 的系统属性,体现在结构上是系统表空间第一个区的第 4 页到第 8 页跟独立表空间的不同(对应页号为:3 ~ 7):
insert buffer header:存储 insert buffer 的头部信息
insert buffer root:存储 insert buffer 的根页
TRX_SYS:存储事务系统相关信息
first rollback segment:第一个回滚段的页
data dictionary header:存储数据字典头部信息
此外,系统表空间的 extent 1 和 extent 2 这两个区(页号 64 ~ 191)也叫双写缓冲区(Doublewrite Buffer),跟事务和 MVCC 有关。
InnoDB 数据字典
InnoDB 除了保存着我们插入的用户数据,还需要保存许多额外的信息,比如:
表所属的表空间
表中有哪些字段、字段对应的类型
表中有哪些索引、索引对应的字段、索引对应的根页面在哪个表空间的哪个页面
表中有哪些外键,外键对应哪个表的哪些字段
表空间对应的文件路径
InnoDB 定义了一些内部系统表(internal system table)来存储上面这些元数据,这些系统表也叫数据字典,如:
其中,最重要的是 SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FIELDS 这 4 张基本系统表(basic system table),只要有了这 4 张表,就意味着可以获取其他系统表以及用户定义的表的所有元数据(如:有哪些列、有哪些索引、某个索引的根页页号是多少) 。
下面,我们来看下这 4 张基本系统表的结构:
SYS_TABLES 表
SYS_TABLES 有两个索引:
以 NAME 列为主键的聚簇索引
ID 列的二级索引
SYS_COLUMNS 表
SYS_COLUMNS 只有一个索引:以 (TABLE_ID, POS) 为主键的聚簇索引
SYS_INDEXES 表
SYS_INDEXES 表只有一个索引:以 (TABLE_ID, ID) 为主键的聚簇索引
SYS_FIELDS 表
SYS_FIELDS 只有一个索引:以 (INDEX_ID, POS) 为主键的聚簇索引
这 4 张表存储了其他表的元数据,那它们自己的元数据谁来存储呢?InnoDB 选择把它们的部分元数据(有哪些列、有哪些索引)硬编码到了代码中,同时把它们的索引的根页页号等信息存储在了系统表的第 8 个页(页号为 7)中,即上图中的 SYS: data dictionary header
页:
Data Dictionary Header:数据字典头部信息,记录基本系统表的根页面位置以及InnoDB存储引擎的一些全局信息
Max Row ID:row_id 的列插入数据时使用该值,全局共享
Max Table ID:每次新建一个表时,就会把该值作为该表的id
Max Index ID:每次新建一个索引时,就会把该值作为该索引的id
Max Space ID:每次新建一个表空间时,就会把该值作为该表空间的id
Mix ID Low:没啥用
Root of SYS_TABLES clust index:SYS_TABLES 表聚簇索引的根页页号
Root of SYS_TABLE_IDS sec index:SYS_TABLES 表为 id 列建立的二级索引的根页页号
Root of SYS_COLUMNS clust index:SYS_COLUMNS 表聚簇索引的根页页号
Root of SYS_INDEXES clust index:SYS_INDEXES 表聚簇索引的根页页号
Root of SYS_FIELDS clust index:SYS_FIELDS 表聚簇索引的根页页号
Segment Header:段头部信息,记录本页面所在段对应的 INODE Entry 位置信息(有 Segment Header 部分,说明专门使用了一个段来给数据字典相关信息分配存储空间)
注意:我们无法直接访问这些内部系统表的数据,但是 information_schema 数据库提供了一些
INNODB_SYS_
开头的表,这些表的数据来源于那些内部系统表: