独立表空间结构
-
区概念
连续的64个页就是一个区,也就是说一个区默认用1MB空间大小。表空间由若干个连续的区(物理位置上的连续)组成的,256的区被划分成一组。
其中,extent0~extent256个区算是第一组,extent256-extent511这256个区算是第二个组,以此类推。- 第一组最开始的3个页面的类型是固定的。
- FSP_HDR:这个类型的页面用来等级整个空间的一些整体属性以及本组所有的区的属性。
- IBUF_BITMAP
- INODE
- 其余各组最开始2个页面的类型是固定的。
- XDES:登记每个区的属性,以及页的状态
- IBUF_BITMAP
- 第一组最开始的3个页面的类型是固定的。
-
段概念
段:某些零散的页面以及一个完整的区的集合。
针对索引:存在叶子节点的区的集合就是一个段,存放非叶子节点的区的集合也算是一个段。
当数据量较少时,即某个索引无法填满区中所有的页,此时使用段来存储索引的话,就会浪费存储空间,所以类似这样的索引,存放在碎片区,碎片区只属于表空间,并不属于任何一个段。而段分配存储空间的策略:- 在刚开始插入数据时,段是从某个碎片区已单页面为单位来分配存储空间的
- 当某个段已经占用了32个碎片区页面后,就会议完整的区为单位来分配存储空间
-
区的分类
空间由若干区组成,
XDES Entry:结构由40个字节构成,大致分为4个部分- Segment ID:表示段的ID,表示该区属于哪一个段。
- List Node:若干个XDES Entry构成一个链表
- Pre Node Page Number 和 Pre Node Offset 的组合就是指向钱一个XDES Entry的指针
- Next Node Page Number 和 Next Node Offset的组个就是指向后一个XDES Entry的指针。
- State:这个字段表明区的状态。
状态 含义 说明 FREE 空闲区 现在还没有用到这个区中的任何页面 FREE_FRAG 有剩余空闲页面的碎片区 表示碎片区中还有可被分配的空闲页面 FULL_FRAG 没有剩余空闲页面的碎片区 表示碎片区中的所有页面都被分配使用,没有空闲页面 FSEG 附属于某个段的区 - Page State Bitmap:占16个字节(128位),其中第1和第2个字节表示一页,第3和第4两个字节表示第二页,一次内推。每个页表示的两个字节中:第一位表示是否是空闲,第二位还未应用到。
XDES Entry 链表
向表中插入数据的本质是想表中各个索引的叶子节点段、非叶子节点段插入数据,如何已较小的开销来处理。首先需要知道哪个段中的哪个区中的哪个页可以插入数据,需要知道区的状态(只有FREE_FRAG和FREE可以插入数据),通过遍历所有的区是可以做到,但是这样的处理方式在数据量在GB时,效率就会降低。Mysql使用了XDES Entry 链表 来处理。- 从表空间中获取空闲的区,用于存储数据
- 通过List Node把状态为FREE的区对应的XDES_Entry结构连成一个链表,这个链表称为FREE链表。
- 通过List Node把装填为FREE_FRAG的区对应的XDES Entry结构连成一个链表,这个链表称为FREE_FRAG链表。
- 通过List Node把状态为FULL_FRAG的区对应的XDES Entry结构连成一个链表,这个链表称为FULL_FRAG链表。
直属表空间的3个链表
- 要知道区属于哪个段的
在Mysql中每个段的区都维护3个链表:
- FREE链表:同一个段中,所有页面都是空闲页面的区对应的XDES Entry结构会被加入到这个链表中。
- NOT_FULL链表:同一个段中,仍有空闲页面的区对应的XDES Entry结构会被加入到这个链表中。
- FULL 链表:同一个段中,没有空闲页面的区对应的XDES Entry结构会被加入到这个链表中。
每个索引都有两个段,而每个段都会维护上述3个链表。
再回到最初的起点 ,捋一捋向某个 段中插入数据时,申请新页面的过程:
1、当一个段的数据较少时,会从free_frag区(空闲页面碎片区)链表取出头结点,并将该XDES Entry节点,并找到该节点对应的区(碎片区),从这个区取一些零散页(通过遍历位图得知哪些页是空闲页)来插入数据。当这个区没有空闲页则修改它的state,并将XDES节点从 free frag链表移到 full_frag 链表。
如果 free_frag 链表没有页面,就从 free 链表移动一个节点到 free_frag 链表,再从这个节点对应的区获取零散页。
一个区的状态改变体现于这个区的XDES节点在不同链表的转移,以及state属性的改变。
2、当这个段已经使用了32个零散页之后,就会进行“碎片整理”(将碎片区的页在内存中进行排序),将这些页数据直接申请完整的区来插入数据,插入后将碎片区的对应的段数据删除。
a 、如果段内的not_full链表不为空,则从not full链表的区申请页;当该区的页全部用完,则该节点移动到段内的 full 链表;
b、如果段内的not_full链表为空,则从段内 Free 链表的区申请页;
b1:如果段内 Free 链表为空,该段可能从表空间申请1个完整的空闲区,或者连续申请多个空闲的区(XDES节点从表空间的Free链表移到段内的 Free链表)。
b2:如果段内 Free 链表不为空,则从该链表的区申请页,并将该节点从 free 链表移到段内的 not_full 链表。
链表基节点
每个表空间独立的3个链表,还有每个段里面的3个链表,都对应一个List Base Node结构,其中:
- List Length 表明该链表一共有多少个结点
- First Node Page Number 和 First Node Offset表明该链表头节点在表空间的位置
- Last Node Page Number 和 Last Node Offset表明该链表尾节点在表空间中的位置。
一般我们把某个链表对应的List Base Node结构放置在表空间中固定的位置,这样想找定位某个链表就变得很简单了。
- 段结的构
段是由若干零散的页面以及一些完成的区组成。
像每个区都有对应的XDES Entry来记录这个区的属性一样,设计者为每个段都定义了一个INODE Entry结构来记录以下段中的属性:
- Segment ID: 就是指这个INODE Entry 结构对应的段的编号
- NOT_FULL_N_USED: 这个字段指的是在NOT_FULL链表中已经使用了多少个页面
- 3个List Base Node: 分别为FREE的链表、NOT_FULL链表、FULL链表定义了List Base Node,这样我们想查找某个段的某个链表的头节点和尾结点的时候,就可以直接找到这个部分找到对应链表的List Base Node!
- Magic Number:这个值是用来标记这个INODE Entry是否已经被初始化了,如果这个数字是97937874, 表明该INODE Entry以已经被初始化,否则没有被初始化。
- Fragment Array Entry:我们前边强调过无数次段是一些零散页面和一些完整的区的集合,每个Fragment Array Entry结构都对应着一个零散的页面,这个结构一共4个字节,表示一个零散页面的页号。
各类型页面详细情况
- FSP_HDR类型
首先看第一个组的第一个页面,当然也是表空间的第一个页面,页号为0,这个页面的类型为FSP_HDR,它存储了表空间的一些整体属性以及第一个组内256个区的对应的XDES Entry结构,直接看这个类型的页面的示意图:
(1)File Space Header部分
从名字就可以看出,这个部分是用来存储表空间的一些整体属性的:
FRAG_N_USED:这个字段表明在FREE_FRAG链表中已经使用的页面数量。
FREE_LIMIT:我们知道表空间都对应着具体的磁盘空间,一开始我们创建表空间的时候对应的磁盘空间都没有数据,所以我们需要对表空间完成一个初始化操作,包括为表空间中的区建立XDES Entry结构,为每个段建立INODE Entry结构,建立链表…的操作,我们一开始就可以为表空间申请一个特别大的空间,但是实际是有绝大部分的区是空闲的,我们可以选择把所有的这些空闲区对应的XDES Entry结构加入FREE链表,也可以选择**只把一部分的空闲区加入FREE链表,等什么时候空闲链表中的 XDES Entry结构不够用了,再把之前没有加入FREE链表的空闲区对应的XDES Entry结构加入FREE链表。 **简单来说,就是什么时候用得到,什么时候再去初始化,InnoDB用的就是后面这个方法。
也就是说,FREE Limit字段表示的页号之前的区都被初始化了,之后的区都还没有。
Next Unused Segment ID:表中每个索引都对应两个段,每个段都有一个唯一的ID,当我们去为某个表新创建一个索引的时候,就意味着需要去创建两个新的段,那么如何为这个新的段找一个唯一的ID呢?Next Unused Segment ID就表示当前表空间中最大的段ID的下一个ID,所以我们如果想要创建新段,直接用这个字段的值作为ID就好啦。
Space Flags:表空间对于一些BOOL类型的属性,或者只需要几个bit就能表示清楚的属性都放在Space Flags中存储,虽然只有4字节32位大小,但是能存储很多表的属性:
POST_ANTELOPE 1 表示文件格式是否大于ANTELOPE ZIP_SSIZE 4 表示压缩页面的大小
ATOMIC_BLOBS 1 表示是否自动把值非常长的字段放到BLOB页里 PAGE_SSIZE 4 页面大小
DATA_DIR 1 表示表空间是否是从默认的数据目录中获取的 SHARED 1 是否为共享表空间 TEMPORARY 1 是否为临时表空间
ENCRYPTION 1 表空间是否加密 UNUSED 18 没有使用到的比特位
XDES类型
每个XDES Entry结构对应表空间的一个区,虽然一个XDES Entry结构只占用40字节,但是表空间的区也非常多,在区的数量庞大的情况下,一个单独的页可能不足以存放足够的XDES Entry结构,所以我们把表空间的区划分为若干个组,每组开头的一个页面记录着本组内所有的区对应的XDES Entry结构。由于第一个区的第一个页面有些特殊,因为它也是整个表空间的第一个页面,除了记录本组内所有区对应的XDES Entry结构以外,还记录着表空间的一些整体属性,这个页面的类型就是我们上面提到的FSP_HDR类型,整个表空间里只有一个这个类型的页面。除去第一个分组以外,之后的每个分组的第一个页面只需要记录本组内所有的区对应的XDES Entry结构即可,不需要在记录表空间的属性了。为了和FSP_HDR类型做区别,我们把之后每个分组的第一个页面定义为XDES,它的结构和FSP_HDR类型非常相似:
与FSP_HDR的类型相比,除了File Space Header部分以外,也就是除了少了记录表空间整体属性的部分以外,其余的部分都是一样的。
IBUF_BITMAP类型
每个分组的第二个页面的类型都是IBUF_BITMAP,这种类型的页里面记录了一些有关Change Buffer的东西,这个Change Buffer里面概念又太多。
INODE 类型
第一个分组的第三个页面是INODe:
List Node for INODE Page List来说,因为一个表空间中可能存在超过85个段,所以可能一个INODE类型的页面不足以存储所有的段对应的INODE Entry结构,因此设计串联成两个不同的链表:
SEG_INODES_FULL链表:该链表中的INODE类型的页面中已经没有空闲空间来存储额外的INODE Entry结构了
SEG_INODES_FREE链表:该链表中的INODE类型的页面中还有空闲空间来存储额外的INODE Entry结构。
这两个链表的基结点就存储在File Space Header里面,也就是说,基节点的位置是固定的。
因此,创建新的段(创建索引就会创建段)的时候,都会创建一个INODE Entry结构与之对应,存储INODE Entry的大致过程如下:
先看看SEG_INODE_FREE链表是否为空,如果不为空,直接从该链表中获取一个节点,也就相当于获取到一个仍有空闲空间 的INODE类型的页面,如何把该INODE Entry结构放到该页面中,如果该页面没有剩余空间了,那么就把该页放到SEG_INODES_FULL链表中。
如果SEG_INODES_FREE链表为空,则需要从表空间的FREE_FRAG链表申请一个页面,修改该页面的类型为INODE,把该页面放到SEG_INODES_FREE链表中,与此同时把INODE Entry结构放入该页面。
Segment Header 结构的运用
我们知道一个索引会产生两个段,一个叶子节点段一个非叶子节点段,而每个段都会对应一个INODE Entry结构,那么我们怎么知道哪个段对应哪个INODE Entry结构呢?当然是要找个地方来记下这个对应关系。INDEX类型的页有一个Page Header部分:
其中PAGE_BTR_SEG_LEAF和 PAGE_BTR_SEG_TOP都占用10个字节,其实他们都对应一个叫Segment Header的结构,图示如下:
PAGE_BTR_SEG_LEAF记录着叶子节点段对应的INODE Entry结构的地址是哪个表空间的哪个页面的哪个偏移量,PAGE_BTR_SEG_TOP记录着非叶子节点段对应的INODE Entry结构的地址是哪个表空间的哪个页面的哪个偏移量。这样子索引和其对应的段的关系就建立起来了。不过需要注意的一点是,因为一个索引只对应两个段,所以只需要在索引的根页面中记录这两个结构即可。
真实表空间对应的文件大小
.ibd文件是自动拓展的,随着表中数据的增多,表空间对应的文件也逐渐增大
系统表空间
系统表空间的结构和独立表空间基本类似,只不过由于整个MySQL进程只有一个系统表空间,在系统表空间中会额外记录一些有关整个系统信息的页面,所以会比独立表空间多出一些的记录这些信息的页面。因为这个表空间最厉害,相当于是表空间之首,所以它的表空间(Space ID)是0。
系统表空间的整体结构
系统表空间与独立表空间的一个非常明显的不同之处就是在表空间开头有许多记录整个系统属性的页面:
由上图可以看到,系统表空间和独立表空间的前三页页面(0,1,2)的类型是一致的,只是3~7的页面是系统表空间特有的,我们来看一下这些页面的作用:
除了这几个以外,系统表空间的extent1和extent2这两个区,也就是页号从64~191这128个页面被称为Doublewrite buffer,也就是双写缓冲区。
InnoDB数据字典
我们平时使用INSERT语句向表中插入的那些记录被称之为用户记录,MySQL只是作为一个软件来为我们报关这些数据,提供方便的增删改查结构而已。但是每当我们向一个表中插入一条记录的时候,MySQL要先校验一下插入语句对应的表存在不存在,插入的列和表中的列是否符合,如果没有语法问题的话,还需要知道该表的聚簇索引和所有二级索引的根页面是哪个表空间的哪个页面,然后把记录插入对应索引的B+树中。所以说,MySQL除了保存着我们插入的用户数据以外,还需要保存许多额外的信息:
某个表属于哪个表空间,表里面有多少列
表对应的每一个列的类型是什么
该表有多少索引,每个索引对应哪几个字段,该索引对应的根页面在哪个表空间的哪个页面
该表有哪些外键,外键对应哪个表的哪些列
某个表空间对应文件系统上的文件路径是什么
…
上述的数据并不是我们INSERT语句所插入的用户数据,而是为了更好的管理用户数据而不得已引入的一些额外数据,这些数据我们也称之为元数据。InnoDB存储引擎特意顶一个一些列的内容系统表来记录这些元数据:
这些系统表也被称为数据字典,它们都是以B+树的形式保存在系统表空间的某些页面中,其中SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FIELDS这四个尤其重要,称之为基本系统表:
SYS_TABLES表
这个表有两个索引:
以NAME列为主键的聚簇索引
以ID列建立的二级索引
SYS COLUMNS表:
这个表只有一个聚簇索引:
以(TABLE_ID,POS)列为主键的聚簇索引
SYS_INDEXES表
这个SYS_INDEXES表只有一个聚簇索引:
以(TABLE_ID,ID)列为主键的聚簇索引。
SYS_FIELDES表
这个表只有一个聚簇索引:
以(INDEX_ID,POS)列为主键的聚簇索引
Data Dictionary Header页面
只要有了上述四个基本系统表,也就意味着可以获取其他系统表以及用户定义的表的所有元数据。比如说我们想看SYS_TABLESPACES这个系统表里存储了哪些表空间以及表空间对应的属性,那就可以:
到SYS_TABLES表中根据表名定位到具体的记录,就可以获取到SYS_TABLESPACES 表的TABLE _ID
使用这个TABLE_ID 到SYS_COLUMNS表中就可以获取到属于该表的所有列的信息。
使用这个TABLE_ID还可以到SYS_INDEXES表中获取所有的索引的信息,索引的信息中包括对应的INDEX_ID,还记录着该索引对应的B+数根页面是哪个表空间的哪个页面。
使用INDEX_ID就可以到SYS_FIELDS表中获取所有索引列的信息。
也就是说这4个表是表中之表,那这4个表的元数据去哪里获取呢?没办法了,只能把这4个表的元数据,就是它们有哪些列、哪些索引等信息硬编码到代码中,然后设计InnoDB的大叔又拿出一个固定的页面来记录这4个表聚簇索引和二级索引对应的B+树位置,这个页面就是页号为7的页面,类型为SYS,记录了Data Dictionary Header,也就是数据字典的头部信息。除了这4个表的5个索引的根页面信息外,这个页号为7的页面还记录了整个InnoDB存储引擎的一些全局属性:
可以看到这个页面由下边几个部分组成:
可以看到这个页面里居然有Segment Header 部分,意味着设计InnoDB的设计者把这些有关数据字典的信息当成一个段来分配存储空间,我们就姑且称为数据字典段吧。由于我们目前需要记录的数据字典信息非常少,所以该段只有一个碎片页,也就是页号为7的这个页。
Data Dictionary Header部分的各个字段:
Max Row ID:我们说过如果我们不显式的为表定义主键,而且表中也没有UNIQUE索引,那么InnoDB存储引擎会默认为我们生成一个名为row_id的列作为主键。因为它是主键,所以每条记录的row_id列的值不能重复。原则上只要一个表中的row_id列不重复就可以了,也就是说表a和表b拥有一样的row_id列也没啥关系,不过设计InnoDB的大叔只提供了这个Max Row ID字段,不论哪个拥有row_id列的表插入一条记录时,该记录的row_id列的值就是Max Row ID对应的值,然后再把Max Row ID对应的值加1,也就是说这个Max Row ID是全局共享的。
Max Table ID:InnoDB存储引擎中的所有的表都对应一个唯一的ID,每次新建一个表时,就会把本字段的值作为该表的ID,然后自增本字段的值。
Max Index ID:InnoDB存储引擎中的所有的索引都对应一个唯一的ID,每次新建一个索引时,就会把本字段的值作为该索引的ID,然后自增本字段的值。
Max Space ID:InnoDB存储引擎中的所有的表空间都对应一个唯一的ID,每次新建一个表空间时,就会把本字段的值作为该表空间的ID,然后自增本字段的值。
Mix ID Low(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表聚簇索引的根页面的页号。
Unused:这4个字节没用,跳过。