前言
大家好,我是xicheng。现在继续更新MySQL,本篇讲InnoDB的表空间,该部分类容比较枯燥繁琐,但又是MySQL后续内容的基础。所以大家可以先学习理解整体框架,等后续篇章用到的时候,再回过头查阅,进一步加深理解。另外,InnoDB的知识脑图如下所示,大家坐稳了。
表空间
表空间(tablespace)由段(sagment)组成,段由区(extent)组成,区由页(page)组成,页由行组成。如下图所示。
所有数据都存放在表空间中。如果用户手动启用了参数innodb_file_per_table,则每张表的数据可以单独放在一个表空间中。
段
逻辑上的概念,⼀个索引会⽣成2个段,⼀个叶⼦节点段(存放叶⼦节点的区),⼀个⾮叶⼦节点段(存放⾮叶⼦节点的区)。
在刚开始向表中插⼊数据的时候,段是从某个碎⽚区(并不是所有页都是存储一个段的数据的区)以⻚为单位来分配存储空间的。
当某个段已经占⽤了32个碎⽚区⻚⾯之后,就会以完整的区为单位来分配存储空间(原先占用的碎片区的页不会被复制到新的区中来)。
常见的段由数据段,索引段,回滚段等。
区结构
对于16KB的页,物理位置连续的64个页就是一个区(extend),大小1MB。256个区被划分为1个组。
每个组中第一个区的固定页如下图所示。
第一组中第0区开始的3个⻚的类型是固定的:
- FSP_HDR(16KB):整个表空间的⼀些整体属性以及本组所有的区,整个表空间只有⼀个该类型的⻚⾯。
- IBUF_BITMAP(16KB):本组所有的区的所有⻚⾯关于INSERT BUFFER的信息。
- INODE(16KB):存储了许多 INODE 的数据结构。
其余各组最开始的 2 个⻚⾯的类型是固定的:
- XDES ( extent descriptor):本组 256 个区的属性。
- IBUF_BITMAP:存储本组所有的区的所有⻚⾯关于 INSERT BUFFER 的信息。
- 在表中数据量⼤的时候,为某个索引分配空间的时候就不再按照⻚为单位分配了,⽽是按照区为单位分配。
区分类
空闲的区:FREE,还没有⽤到这个区中的任何⻚⾯。
有剩余空间的碎⽚区:FREE_FRAG,表示碎⽚区中还有可⽤的⻚⾯。
没有剩余空间的碎⽚区:FULL_FRAG,表示碎⽚区中的所有⻚⾯都被使⽤,没有空闲⻚⾯。
附属于某个段的区:FSEG。
区的XDES Entry
结构
为了方便管理区而设计的。共40个字节,分为4个部分。
- SegmentID(8字节):段唯一编号,表示就是该区所在的段(前提是该区已被分配给某段了,否则该字段无意义)。
- ListNode(12字节):PreNodePageNumber(4字节,前一页的页号)和PreNodeOffset(2字节,前一页的页号在页内的偏移量)指向前⼀个XDESEntry。NextNodePageNumber(4字节,后一页的页号)和NextNodeOffset(2字节,后一页的页号在页内的偏移量)指向后⼀个XDESEntry。
- State:区的状态。参见“InnoDB表空间-区分类”条目。
- PageStateBitmap:128个⽐特位。每2个⽐特位对应区中的⼀个⻚。第⼀个位表示对应的⻚是否是空闲的(1空闲。0不空闲),第⼆个⽐特位还没有⽤(1没用。0用了)。
XDES Entry链表
通过List Node把FREE区对应的XDES Entry链接成一个链表,叫FREE链表。同一段中所有页面是空闲的区的XDES Entry会被加到这个链表。
通过List Node把FREE_FRAG区对应的XDES Entry链接成一个链表,叫FREE_FRAG链表。同一段中还有空闲页面区的XDES Entry会被加到这个链表。
通过List Node把FULL_FRAG区对应的XDES Entry链接成一个链表,叫FREE_FRAG链表。同一段中没有空闲页面区的XDES Entry会被加到这个链表。
每个XDES Entry链表会有一个List Base Node节点,会被放在段的INODE Entry。其中。
- ListLength:该链表总节点数。
- FirstNodePageNumber和FirstNodeOffset:该链表的头节点在表空间中的位置。
- LastNodePageNumber和LastNodeOffset:该链表的尾节点在表空间中的位置。
段的INODE Entry
为了方便管理段而设计的。共192字节,被分为如下几个部分。
- Segment ID(8字节):该INODE Entry对应的段号。
- NOT_FULL_N_USED(4字节):NOT_FULL链表的各XDES Entry节点对应的区已经使⽤了多少⻚⾯。⼀个区中有64个⻚⾯,如果不标记已经使⽤了多少⻚⾯的话,每次向段中插⼊数据的时候都要从第⼀个⻚⾯进⾏遍历寻找空闲⻚⾯,有了这个字段之后就可以快速定位空闲⻚⾯。
- 3个List Base Node(分别都为16字节):分别为段的FREE链表、NOT_FULL链表、FULL链表定义了ListBaseNode。
- Magic Number:标记这个INODE Entry是否已经被初始化了(值是97937874,表明已经初始化,否则没有被初始化)。
- Fragment Array Entry:段是由零散的页面和完整的区组成。每个Fragment Array Entry结构都对应着⼀个零散的⻚⾯,这个结构⼀共4个字节,表示⼀个零散⻚⾯的⻚号。
页类型
- FSP_HDR类型
表空间的第一个页面,也是第一个组的第一个页面,页号为0,存储表空间的整体属性及第一个组内内256区对应的XDES Entry结构。如下表所示。
名称 | 描述 | 占用空间(字节) | 作用 |
---|---|---|---|
File Header | ⽂件头部 | 38 | 页的通用信息 |
File Space Header | 表空间头部 | 112 | 表空间的⼀些整体属性信息 |
XDES Entry | 区描述信息 | 10240 | 存储本组256个区对应的属性信息 |
Empty Space | 尚未使⽤ 空间 | 5986 | ⻚结构的填充 |
File Trailer | ⽂件尾部 | 8 | 校验⻚是否完整 |
File Space Header如下表所示。
名称 | 占用空间(字节) | 描述 |
---|---|---|
Space ID | 4 | 表空间的ID |
Not Used | 4 | 未使⽤ |
Size | 4 | 当前表空间占有的⻚⾯数 |
FREE Limit | 4 | 尚未被初始化的最⼩⻚号,⼤于或等于这个⻚号的区对应的XDES Entry结构都没有被加⼊FREE链表 |
Space Flags | 4 | 表空间的⼀些占⽤存储空间⽐较⼩的属性,不同MySQL版本有些差异 |
FRAG_N_USED | 4 | FREE_FRAG链表中已使⽤的⻚⾯数量 |
3个List Base Node | 16/16/16 | FREE/FREE_FREG/FULL_FREG链表的基节点 |
Next Unused Segment ID | 8 | 当前表空间中下⼀个未使⽤的Segment ID |
List Base Node for SEG_INODES_FULL List | 16 | SEG_INODES_FULL链表的基节点 |
List Base Node for SEG_INODES_FREE List | 16 | SEG_INODES_FREE链表的基节点 |
- XDES类型
除了第一个分组的第一个页面是FSP_HDR类型之外,之后的每个分组的第⼀个⻚⾯只需要记录本组内所有的区对应的XDES Entry结构即可。就叫它XDES类型。如下图所示。
- IBUF_BITMAP类型
每个分组的第⼆个⻚⾯的类型都是这种类型,这种类型的⻚⾥边记录了⼀些有关Change Buffer的信息。本质是一颗B+树。
在修改非唯一二级索引页面时,如果页面尚未被加载到内存中,那么该修改会被暂时存储到Change Buffer中,等服务器空闲或者对应页面从磁盘加载到内存时,再将修改合并到对应的页面。
- INDOE类型
第⼀个分组的第三个⻚⾯。记录段的相关信息,如下表所示。
名称 | 描述 | 占用空间(字节) | 作用 |
---|---|---|---|
File Header | ⽂件头部 | 38 | 页的通用信息 |
List Node for INODE Page List | 通用链表节点 | 12 | 存储上下两个INODE页面的指针。如果一个表空间超过85个INODE Entry,则需要额外的该类型页面来存储。 |
INODE Entry | 段描述信息 | 16320 | 具体的INODE Entry结构 |
Empty Space | 尚未使⽤空间 | 6 | ⻚结构的填充 |
File Trailer | ⽂件尾部 | 8 | 校验⻚是否完整 |
INDOE类型页面被划分为两个链表:
- SEG_INODES_FULL链表:该链表中的INODE类型的⻚⾯中已经没有空闲空间来存储额外的INODE Entry结构了。
- SEG_INODES_FREE链表:该链表中的INODE类型的⻚⾯中还有空闲空间来存储额外的INODE Entry结构了。
新建段时,会创建INODE Entry,存储INODE Entry的过程如下:
- 先看SEG_INODES_FREE链表是否为空,若不为空,直接从该链表中获取节点(页面),并把新的INDODE Entry放进去。当节点(页面)无空余空间时,就把该节点(页面)放到SEG_INODES_FREE中去。
- 若SEG_INODES_FREE为空,则需要从表空间的FREE_FRAG链表中申请一个页面,并将该页面的类型修改为INODE,并加入SEG_INODES_FREE链表,然后把INODE Entry结构放入该页面。
Segement Header结构
数据页的Page Header中有这两个字段:PAGE_BTR_SEG_LEA(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 Entry | 2 |
系统表空间
独立表空间用于记录用户数据(上述内容都是讲的独立表空间),系统表空间用于记录一些与整个系统有关的信息。
系统表空间表空间ID(SpaceID)是0。
第一个区的前三个页面与独立表空间是一致的,但第4个页面到第8个页面(页号从3到7)是系统表空间独有的,如下表所示。
页号 | 页面类型 | 英文名称 | 作用 |
---|---|---|---|
3 | SYS | Insert Buffer Header | 存储Insert Buffer的头部信息 |
4 | INDEX | Insert Buffer Root | 存储Insert Buffer的根⻚⾯ |
5 | TRX_SYS | Transction System | 事务系统的相关信息 |
6 | SYS | First Rollback Segment | 第⼀个回滚段的⻚⾯ |
7 | SYS | Data Dictionary Header | 数据字典头部信息,下文会讲到 |
后续区的前两个页面与独立表空间对应的区的页面是一致的。
元数据
更好地管理用户数据而引入的额外数据称为元数据。
记录元数据的系统表如下表所示。用户不能直接访问InnoDB的系统表。
表名 | 作用 |
---|---|
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存储引擎中所有的虚拟⽣成列的信息 |
如下四个表尤为重要,且这四个表的元数据硬编码到代码中了。
- SYS_TABLES表
(NAME为主键,ID列为二级索引)
表名 | 作用 |
---|---|
NAME | 表名 |
ID | InnoDB存储引擎中每个表都有⼀个唯⼀的ID |
N_COLS | 该表拥有列的个数 |
TYPE | 表的类型,记录了⼀些⽂件格式、⾏格式、压缩等信息 |
MIX_ID | 忽略 |
MIX_LEN | 忽略 |
CLUSTER_ID | 忽略 |
SPACE | 该表所属表空间的ID |
- SYS_COLUMNS表
(以TABLE_ID,POS为主键)
表名 | 作用 |
---|---|
TABLE_ID | 该列所属表对应的ID |
POS | 该列在表中是第⼏列 |
NAME | 列的名称 |
MTYPE | 表的类型,记录了⼀些⽂件格式、⾏格式、压缩等信息 |
PRTYPE | 主数据类型,例如INT、VARCHAR |
LEN | 该列最多占⽤存储空间的字节数 |
PREC | 忽略 |
- SYS_INDEXES表
(TABLE_ID,ID为主键)
表名 | 作用 |
---|---|
TABLE_ID | 该索引所属表对应的ID |
ID | InnoDB存储引擎中每个索引都有⼀个唯⼀的ID |
N_FIELDS | 该索引包含列的个数 |
TYPE | 索引的类型,⽐如聚簇索引、唯⼀索引 |
SPACE | 该索引根页面所在的表空间ID |
PAGE_NO | 该索引根页面所在页号 |
MERGE_THRESHOLD | 如果⻚⾯中的记录被删除MERGE_THRESHOLD,就把该⻚⾯和相邻⻚⾯合并 |
- SYS_FIELDS表
(INDEX_ID,ID为主键)
表名 | 作用 |
---|---|
INDEX_ID | 该列所属索引的ID |
POS | 该列在索引中是第几列 |
COL_NAME | 列名 |
Data Dictionary Header页面:用固定的⻚⾯来记录上述4个表的聚簇索引和⼆级索引对应的B+树位置,这个⻚⾯就是⻚号为7的⻚⾯。如下图与表所示。
名称 | 描述 | 占用空间(字节) | 作用 |
---|---|---|---|
File Header | ⽂件头部 | 38 | 页的通用信息 |
Data Dictionary Header | 数据字典头部 | 52 | 记录⼀些基本系统表的根⻚⾯位置以及InnoDB存储引擎的⼀些全局信息 |
Unused | 未使用 | 4 | 未使用 |
Segment Header | 段头部 | 10 | 记录本⻚⾯所在段对应的INODEEntry位置信息 |
Empty Space | 尚未使⽤空间 | 6 | ⻚结构的填充 |
File Trailer | ⽂件尾部 | 8 | 校验⻚是否完整 |
Data Dictionary Header详解
- MaxRowID:不论哪个拥有row_id列(InnoDB隐藏列)的表插⼊⼀条记录时,该记录的row_id列的值就是MaxRowID对应的值,然后再把MaxRowID对应的值加1,也就是说这个MaxRowID是全局共享的。
- MaxTableID:InnoDB存储引擎中的所有的表都对应⼀个唯⼀的ID,每次新建⼀个表时,就会把本字段的值作为该表的ID,然后⾃增本字段的值。
- MaxIndexID:InnoDB存储引擎中的所有的索引都对应⼀个唯⼀的ID,每次新建⼀个索引时,就会把本字段的值作为该索引的ID,然后⾃增本字段的值。
- MaxSpaceID:InnoDB存储引擎中的所有的表空间都对应⼀个唯⼀的ID,每次新建⼀个表空间时,就会把本字段的值作为该表空间的ID,然后⾃增本字段的值。
- MixIDLow(Unused):无用。
- 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表聚簇索引的根⻚⾯的⻚号。
结尾
InnoDB的表空间就讲完了,希望大家能持续学习。下一篇MySQL文章讲InnoDB-数据目录。
微信扫描下方二维码,或搜索“xicheng”,关注公众号后回复【笔记】,有我准备的15万字Java面试笔记。
感谢各位人才的点赞、收藏和评论,干货文章持续更新中,下篇文章再见!