InnoDB是以页为单位管理存储空间的,在InnoDB中针对不同的目的设计了各种不同类型的页面。如下(省略了FIL_PAGE或FiL_PAGE_TYPE的前缀):
**区是由连续页组成的空间,在任何情况下每个区的大小都为1MB。**无论是系统表空间还是独立表空间,都可以看成是由若干个连续的区组成的。
区是用于向磁盘申请存储空间的单位。之所以引入区的概念,是因为如果以页尾单位来分配存储空间,双向链表(B+树每一层的页都会形成一个双向链表)相邻的两个页的物理位置可能离的非常远。为了尽量让页面链表中相邻的页的物理位置也相邻,使得每次扫描节点中的记录时可以使用顺序IO,所以每次会申请一块更大的存储空间(即区),之后页从区中分配,而不是每次都以单独的页去申请。
InnoDB中在区之上又引入了段的概念,段其实不对应表空间中某一个连续的物理区域,它是一个逻辑上的概念,由若干个零散的页面和一些完整的区组成。实际上就是对表空间中的页进行分类,将存放相同类型数据的页统筹到一起进行管理,例如undo页便组成了回滚段。此外每个索引也按照B+树的叶子结点和非叶子节点进行区别对待,也就是说叶子节点有自己独有的区,非叶子节点也有自己独有的区。存放叶子节点的区的集合就算是一个段,存放非叶子结点的区的集合也是一个段。也就是说一个索引会生成两个段:一个叶子结点段和一个非叶子节点段。
由于一个区默认是占用1MB的存储空间,对于存储记录比较少的表而言,也许根本用不完这么多的空间。为了节省空间,InnoDB引入了碎片区的概念,在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,碎片区中的页可以用于不同的目的,有些属于段A、有些属于段B,有些甚至不属于任何段。碎片区直属于表空间,并不属于任何一个段。
段进行空间分配的策略如下:
这也就是最开始所说的,段由若干个零散的页面和一些完整的区组成。
InnoDB中的页有两个固定的部分:
File Header和File Trailer中间的部分根据类型的不同有着不同的结构,而这两个部分是所有页面统一的。
File Header 是用来记录各种页都适用的一些通用信息,由8个部分组成:
表空间中的每一个页都对应着一个页号,表空间中第一个页的页号为0,之后的也好分别是1、2、3等。某些类型的页可以通过FIL_PAGE_PREV和FIL_PAGE_NEXT组成链表(主要用于索引页/数据页,B+树的特性决定的),链表中相邻的两个页面的页号可以不连续。
File Trailer用于校验是否完整,保证页面从内存刷新到磁盘后内容是相同的,由2个部分组成:
如果页面刷新成功,则页首和页尾的校验和以及LSN应该是一致的。如果刷新了一部分后断电了,那么头部的校验和就代表着已经修改过的页,而尾部的校验和代表着原先的页,两者不同则说面刷新期间发生了错误。
FREE、FREE_FRAG、FULL_FRAG这3种状态的区都是独立的,算是直属于表空间,而处于FSEG状态的区是附属于某个段的。
为了方便管理这些区,InnoDB中有一个称为XDES Entry的结构。每一个区都对应着一个XDES Entry结构,这个结构记录了对应的区的一些属性。
XDES Entry结构有40字节,大致分为4个部分:
Segment ID(8字节):每一个段都有一个唯一的编号,标志当前区是分配给哪个段的
List Node(12字节):这个部分可以将若干个XDES Entry结构串成一个链表
State(4字节):表名区的状态,即前面提到的FREE、FREE_FRAG、FULL_FRAG和FSEG
Page State Bitmap(16字节):划分为64个部分,每个部分有2位,对应区中的一个页。其中第一位表示对应的页是否空闲,第二位还没有用到
由于表空间是可以不断增大的,不可能每次查找区的的时候都去遍历整个表所有区的XDES Entry。所以对于直属于表空间的三种类型的区,使用List Node维护了对应的三种类型(FREE、FREE_FRAG、FULL_FRAG)的链表来方便查找,之后使用时直接从对应的链表的头结点取即可。对于直属于段的区,也维护了三种类型(FREE、NOT_FULL、FULL)的链表。
如果一个表有两个索引,那么将会有4个段。对于每个段,都需要维护三种类型的链表,再加上直属于表空间的3个链表,总共需要维护15个类型的链表。
使用链表可以将不同类型的区给区分出来,那我们首先还需要找到链表的头结点才可以使用。因此前面介绍的**每个链表都会对应有一个List Base Node结构,这个结构中包含了链表的头节点和尾节点的指针和这个链表包含了多少个节点的信息。**List Base Node结构会放置在表空间中的一个固定位置,那么要查找某种类型的区时就直接去这个固定的位置拿到List Base Node然后拿到它的头节点即可。
像每个区都有一个对应的的XDES Entry结构一样,每个段也都定义了一个INDOE Entry结构来记录段中的信息。INODE Entry的属性如下:
在MySQL5.6.6及以后的版本中,**InnoDB不再默认把各个表的数据存储到系统表空间,而是为每个表建立一个独立表空间。**也就是创建了多少个表就有多少个独立表空间。在使用独立表空间来存储数据时,会在该表所属数据库对应的子目录下创建一个表示该独立表空间的文件,其文件名和表名相同,文件后缀为.ibd。
前面我们讲到了区和段的概念,也讲到了它们对应的XDES Entry结构和INODE Entry结构以及各种以XDES Entry为节点的链表。我们知道了页的申请需要用到这些结构,那这些结构存放在那呢,这就需要了解整个表空间的结构了。
表空间实际上就是由许许多多的页组成的,为了更好的管理页,我们引入了区的概念,对于16KB的页而言,64个连续的页面就是一个区。而表空间可以看为是由若干个连续的区组成,其中每256个区被划分成了一组(同样是为了更好的管理),每个组中的前几个页面会负责维护这个组的一些信息。
第一个组最开始的三个页面的类型是固定的
其余所有组的前两个页面的类型是固定的:
这是表空间的第一个页面,页号为0,类型为FSP_HDR,其组成如下:
FIle Header:页的通用结构,不再赘述
File Space Header:表空间头部,用来描述表空间的一些整体属性信息
表空间ID
表空间拥有的页面数
尚未被初始化的最小页号,大于或等于这个页号的区对那个的XDES Entry结构都没有被加入FREE链表(对于表空间而言,可能在初始化或者自增长时分配的磁盘空间很大,而这些磁盘空间的空闲区并没有直接加入到FREE链表,而是等到需要使用时在初始化加入到FREE链表中)
FREE_FRAG链表中已使用的页面数量
FREE、FREE_FRAG、FULL_FRAG链表的基节点
表空间中下一个未使用的段ID(每次创建新段时从这里获取ID,使用后将该值加一即可)
SEG_INODES_FULL和SEG_INODES_FREE链表的基节点
XDES Entry:区描述信息,存储本组256个区对应的属性信息
Empty Space:尚未使用的空间
File Trailer:页的通用结构,不再赘述
除了第一个组,其余每组第一个页面都是XDES类型的页面,其中存放了这个组内所有区对应的XDES Entry。
与FSP_HDR类型的页面相比,XDES类型的页面除了没有File Space Header部分之外,其余部分都是一样的。
每个分组的第二个页面的类型都是IBUF_BITMAP,这种类型的页面中记录了一些有关Change Buffer的信息。
我们平时向表中插入一条记录,实际本质就是向每个索引对应的B+树插入记录。该记录首先插入聚簇索引页面,然后再插入每个二级索引页面。这些页面在表空间中随机分布,将会产生大量的随机I/O,严重影响性能,修改和删除也是同理。因此InnoDB引入了Change BUffer结构(本质上也是表空间中的一棵B+树,它的根节点存储在了系统表空间中),在修改非唯一二级索引页面时(原因可以自行了解关于Change Buffer的内容),如果该页面尚未被加载到内存中,那么该修改将先被暂时缓存到Change Buffer中,之后服务器空闲或者因为其他原因导致对应的页面加载到内存时,再将修改合并到对应页面。
**第一个分组中的第三个页面的类型是INODE,前面提到的每个段对应的INODE Entry便存放在这个页中。**其组成如下:
当我们每创建一个新的段时(创建索引就会创建段),都会创建一个与之对应的INODE Entry,其流程如下:
表空间中除了前面提到的每个组前几个固定的页面,剩下大多数存放的都是INDEX页,也就是B+树的节点。我们知道B+树中叶子节点存放的是数据,而非叶子节点存放的是索引。但其实无论是聚簇索引还是二级索引,无论是叶子节点还是非叶子节点,都是使用这个类型的页面。其结构如下:
File Header:页的通用结构,不再赘述(数据页之间没有必要是物理连续的,因为这个头部中有双向链表来维护页的顺序)
Page Header:
Infimum 和 Supremum Records:最小记录和最大记录(两个虚拟的记录)
User Record:实际存储行记录的内容
Free Space:空闲空间,同样也是个链表数据结构。在一条记录被删除之后,该空间会被加入到空闲链表中
Page Directory:页目录
File Trailer:页的通用结构,不再赘述
我们都知道一个索引会产生两个段,分别是叶子节点段和非叶子节点段,而每个段都会对应一个INODE Entry结构。那我们怎么知道某个段对应哪个INODE Entry结构呢?**其实索引对应的段的INODE Entry结构就存放在这个索引INDEX页面的Page Header中的Page_BTR_SEG_LEAF(B+树叶子节点段的头部信息)和PAGE_BTR_SEG_TOP(B+树非叶子段的头部信息)属性中(只会在B+树的根页中定义)。**这两个属性其实都对应一个Segment Header的结构,具体结构如下:
行记录的记录头信息:固定占用 5 个字节,40 位
预留位(1位)
预留位(1位)
deleted_flag(1位):该行是否已被删除
- 记录删除之后并不会从磁盘移除,因为移除还需要在磁盘上重新排列其他的记录,所以只将其标记为1
- 被删除掉的记录会形成一个垃圾链表,记录在这个链表中占用的空间称为可重用空间
min_rec_flag(1位):B+ 树每层非叶子节点中最小的目录项记录都会添加该标记
n_owned(4位):每个页的记录会被分组,分组的owner(组内最大记录)会记录该组的记录数量
heap_no(13位):索引堆中该条记录的排序记录
- 向表中插入的记录本质上来说都是放到User Records部分,这些记录一条一条紧密的排列在一起
- 把每一条记录(包括deleted_flag为1的记录)在堆中的相对位置称之为heap_no。在页中靠前的记录heap_no相对较小,靠后的记录heap_no相对较大。每申请一条新的记录的存储空间时,该记录比物理位置在它前面的那条记录的heap_no值大一
record_type(3位):0表示普通记录,1表示非叶子节点的目录项记录(即索引),2表示Infimum记录,3表示Supremum记录
next_record(16位):下一条记录的相对位置
- 下一跳记录指的并不是插入顺序中的下一条,而是按照主键值有小到大的顺序排列的下一条记录
- InnoDB始终会维护记录的一个单项链表,链表中的各个节点都是按照主键值从小到大的顺序连接起来的
- 目录项中的槽就是按照链表的顺序划分的,从而保证一个有序性
- 被删除的记录也会使用该属性连接形成垃圾链表
对于普通记录(叶子结点)和目录项记录(非叶子结点/索引),都是采用一样的行记录格式,只有如下部分有所差别:
- 目录项的record_type是1,普通记录是0
- 目录项记录只有主键值和页号两个列(二级索引有3个列:索引列的值、主键值和页号,先按照索引列排序,之后在按照主键值排序),而普通记录的列是用户自己定义的,可能包含很多了,还有InnoDB自己添加的隐藏列
- 只有目录项记录的min_rec_flag属性才可能为1,普通记录的都是0
在默认情况下,InnoDB会在数据目录下创建一个名为ibdata1、大小为12MB的文件,这个文件就是系统表空间在文件系统上的表示。这个文件是一个字扩展文件,当不够用时会自动自己增加文件大小。
前面我们知道了每个独立表空间中都划分成了一些组,每个组包含了256个区,组的前几个页面为保存有区对应的XDES Entry结构,而表空间的第一个页面还存放了直属于表空间的各种类型的区的链表以及表中各个段对应的INODE Entry结构。那么此时我们如果在添加数据时需要申请页面,就可以根据当前所操作的这棵B+树,找到其对应的段的INODE Entry,如果其维护的零碎的页面还没有超过32个,那么就会从直属于表空间的区的链表中找到合适的区去申请页面。如果已经超过了,那么就从直属于这个段的区的链表中找到合适的区申请页面。
前面操作的前提都是我们需要能拿到我们要操作的B+树的根节点,那怎么找到要插入的数据所在的B+树呢。在独立表空间中其实并没有维护哪个索引就是对应哪个数据页之类的信息,这部分信息是存储在系统表空间中的。
系统表空间和独立表空间的前三个页面的类型是一致的(FSP_HDR、XDES、IBUF_BITMAP),后续其他组的前两个页面类型也是一致的(XDES、IBUF_BITMAP)。但是页号3-7的页面(即第一个组的第四到第八个页面)是系统表空间特有的,如下:
除了这几个记录系统属性的页面外,系统表空间第二个区和第三个区(即extent1和exten2,也就是页号从64-191这128个页面)称为Doublewrite Buffer(双写缓冲区)。
为了解决我们上面提到的问题,这里我们主要了解数据字典。
当我们向一个表中插入一条记录时,MySQL先要校验插入语句所对应的表是否存在,以及插入的列和表中的列是否符合,如果语法没有问题,还需要知道表的聚簇索引和所有二级索引对应的根页面是哪个表空间的哪个页面,然后再把记录插入到对应索引的B+树中。所以我们除了在独立表空间中保存用户插入的数据外,还需要在系统表空间中保存许多额外的信息:
上面这些信息并不是使用iinsert语句插入的用户数据,而是为了更好地管理用户数据而不得不引入的一些额外数据,也称为元数据。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存储引擎中所有虚拟生成的列的信息
这些系统表也被称为数据字典,它们都是以B+树的形式保存在系统表空间的某些页面中。其中SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FIELDS这四个表尤为重要,称为基本系统表。
只要有了上述4个基本系统表,就可以获取其他系统表以及用户定义的表的所有元数据。也就是说这4个表示表中之表。
那么这四个表的元数据(比如说它们有哪些页,那些索引等信息)去哪获取呢,这里只能硬编码到代码中了,然后将前面讲到的系统表空间的页号为7的页面,即Data Dictionary Header用来保存数据字典头部信息。这个页中记录了这4个基本系统表的聚簇索引和二级索引对应的B+树的位置以及一些全局属性,具体如下:
File Header:页的通用结构,不再赘述
Data Dictionary Header:数据字典头部,记录一些基本系统表的根位置以及InnoDB存储引擎的一些全局信息
Max Row ID:隐藏列row_id的值,全局共享的
Max Table ID:新建表时使用的ID,全局共享的
Max Index ID:新建索引时使用的ID,全局共享的
Max Space ID:新建表空间时使用的ID,全局共享的
SYS_TABLES表聚簇索引的根页面的页号
SYS_TABLES表二级索引的根页面的页号
SYS_COLUMS表聚簇索引的根页面的页号
SYS_INDEXES表聚簇索引的根页面的页号
SYS_FIELDS表聚簇索引的根页面的页号
Unused:未使用
Segment Header:段头部,记录了本页面所在段对应的INODE Entry位置信息
Empty Space:尚未使用的空间
File Trailer:页的通用结构,不再赘述
用户其实不能直接访问InnoDB的这些内部系统表,除非直接去解析系统表空间对应的文件系统的文件,不过InnoDB在系统数据库information_schema提供了一些以INNODB_SYS开头的表以供访问。这些INNODB_SYS开头的表并非真正的内部系统表,而是存储引擎启动时读取系统表后填充进去的,所以它们与系统表的字段并不完全一样。