InnoDB磁盘结构详解

表空间

磁盘部分包括各种表空间,包括系统表空间(System Tablespace)、独立表空间(File-Per-Table Tablespaces)、undo表空间(Undo Tablespaces)、通用表空间(General Tablespaces)、临时表空间(Temporary TableSpaces)5种表空间。

表空间可以看做是InnoDB存储引擎逻辑结构的最高层 ,所有的数据都是存放在表空间中。InnoDB通过参数InnoDB_file_per_table(DMS是ON)可以选择使用系统表空间还是独立表空间存储表,如果不是ON,则所有InnoDB表都保存在ibdata1这个表文件中,否则一个表占据一个表文件,拥有自己独立的表文件(用户记录、索引和插入缓冲Bitmap),即每个Table单独存储为一个“.ibd”文件,但change buffer等依然存放在系统表空间。

多个段组成一个表空间。常见的段有数据段、索引段、回滚段等,段是一个逻辑的概念,是一些零散页面和一些完整的区的集合。不同类型的数据保存在单独的段内,可以更好的保持该类型数据的连续性,可以提升访问磁盘的效率。创建一个索引会创建数据段和索引段,即一个索引占用两个段。

  • 数据段:B+树的叶子节点(Leaf node segment)
  • 索引段:B+树的非叶子节点(Non-leaf node segment)
  • 回滚段(rollback segment):InnoDB中undo log是采用分段(segment)的方式进行存储的,每一个rollback segment内部由1024个undo segment组成,每个undo Tablespace最多会包含128个rollback segment。每一时刻一个undo segment都是被一个事务独占的,每个写事务都会持有至少一个undo segment,当有大量写事务并发运行时,就需要存在多个undo segment。MySQL 8.0由于支持了最多128个独立的Undo Tablespace,一方面避免了ibdata1的膨胀,方便undo空间回收,另一方面也大大增加了最大的rollback segment的个数,增加了可支持的最大并发写事务数(128*128*1024)。

注意,虽然InnoDB区分了数据段和索引段,但由于数据是以主键为索引来组织数据的存储的,所以索引文件和数据文件都在同一个文件中,都在“.ibd”文件里面。

表空间中的页实在是太多了,为了更好的管理这些页面,InnoDB提出了区的概念。一个表空间划分为多个区(extent),一个区内包含物理上连续的64个页,因此一个区空间大小为64*16KB=1M。区就是为了保证页的连续性,InnoDB一次会从磁盘申请4~5个区。

段可以简单理解为是一个逻辑的概念,而Extent是一个物理概念,每次B+树的扩容都是以Extent为单位来扩容的,默认一次扩容不超过4个Extent。

段区分了数据段和索引段,其实也就有了各自的区,即叶子节点和非叶子节点都有自己独立的区。想象一下,当B+树按顺序范围查询时,如果数据分布在磁盘的不同位置,就会产生随机IO,而如果数据的物理位置相邻,就可以通过顺序IO读取了。

页是InnoDB中管理数据的最小单元,是固定大小的一段连续磁盘空间,默认为16KB,用于存放数据、索引等各种类型的数据。

InnoDB中,常见的页类型有数据索引页、undo page、文件管理页FSP_HDR/XDES、插入缓冲IBUF_BITMAP页、INODE页等。

在InnoDB中的设计中,页与页之间是通过一个双向链表连接起来,而存储在页中的数据行则是通过单链表连接起来的,如下图:

图片[1]-InnoDB磁盘结构详解-不念博客
双向链表

页有通用的文件头和尾(将页的内容进行封装,通过文件头和文件尾的checksum方式来确保页的完整性),但是中部的内容根据页的类型不同而发生变化。我们主要关注数据页和索引页,这种类型的页包括七个部分:

  • File Header:文件头,共38B,记录了页的地址、页号、上一页和下一页指针、页的类型信息、页的校验和checksum(校验和在写入磁盘前计算得到,当从磁盘中读取时,重新计算校验和并与数据页中存储的对比,如果发现不同,则会导致MySQL crash)、日志序列位置(LSN,Log Sequence Number,表示日志文件的长度,一个不断递增的unsigned long类型整数)等。
  • Page Header:数据页头,用来记录数据页的状态信息,包括Free Space的地址、本页中的记录的数量、标记为删除的记录等,共56B。
  • System records:Infimum + Supremum Records。InnoDB每页中有两个虚拟的行记录,用来限定记录的边界。Infimum记录是比该页中任何主键值都要小的记录,Supremum记录是比该页中任何主键值都要大的记录。这两个记录在页创建时被建立,并且在任何情况下不会被删除,并且由于这两条记录不是我们自己定义的记录,所以它们并不存放在页的User Records部分。所以如果数据是顺序存储的,那么查询数据是否在某一页中就无需遍历页中的所有数据,只需判断这两个记录就行了。
  • User Records:用户记录,以单链表的形式存储,如下图:
图片[2]-InnoDB磁盘结构详解-不念博客
单链表
  • Free Space:空闲空间,用于存放新记录。在一开始生成页的时候,并没有User Records这个部分,每当插入一条记录,就会从Free Space部分中申请一个记录大小的空间到User Records部分,当Free Space用完时,这个页也就使用完了。
  • Page Directory:数据目录(弥补单向链表查询性能差的缺点),InnoDB会把页中的记录划分为若干个组,每个组的最后一个记录的地址偏移量作为一个槽,存放在Page Directory中,便于二分查找定位数据。对于分组中的记录数是有规定的:Infimum记录所在的分组只能有 1 条记录,Supremum记录所在的分组中的记录条数只能在1~8条之间,中间的其它分组中记录数只能在是4~8条之间。所以如果数据是顺序存储的,那么查询数据在某一页的位置就无需遍历页中的所有数据,只通过二分法就可以快速定位到对应的槽,然后再遍历该槽对应分组中的记录就能知道了。
  • File Trailer:文件尾,共8B,包括页的校验和checksum(依赖于引擎选用的校验算法,不一定与文件头的checksum相同)、日志序列位置(LSN),与File Header中的相同。默认情况下,InnoDB每次从磁盘读取一个页就会检测该页的完整性,即File Trailer中的内容需和File Header保持一致。

数据行即一行一行的数据。MySQL中单行数据最大能存储64KB=65535B,故表中字段长度加起来如果超过该值就会拒绝创建表。以utf8mb4字符集下varchar(M)为例,该字符集下一个字符最多需要4B表示,如果M大于16383,那么总字节数就会超过4*16383=65532B,所以M的最大值就是16383个字符。

虽然单行数据最大值远大于单页(16KB),但MySQL为了在单页中至少存储2行数据(每行8KB),引入了行溢出机制,即只要一行记录的总和超过8KB,就会溢出,比如varchar(9000) 或者 varchar(3000) + varchar(3000) + varchar(3000),当实际长度大于8k的时候,会对最大字段使用uncompress BLOB page单独存储(即一个字段独享一个或多个页),而在Barracuda文件格式下字段本身只会用20B存储溢出行的地址和占用的字节数。

InnoDB的文件格式包括旧格式Antelope和新格式Barracuda(DMS使用该格式),两者主要的不同在于对存储数据时所占用的空间差异,每种文件格式有自己支持的行格式,行格式就是指数据行的存储方式,包括是否紧凑存储(占用磁盘空间)、是否可变长度存储、大索引前缀支持、压缩支持。差异如下:

行格式紧凑的存储特性增强的可变长度列存储大索引键前缀支持压缩支持支持的表空间类型所需文件格式
REDUNDANT(冗余)system, file-per-table, generalAntelope or Barracuda
COMPACT(紧凑)system, file-per-table, generalAntelope or Barracuda
DYNAMIC(动态)system, file-per-table, generalBarracuda
COMPRESSED(压缩)file-per-table, generalBarracuda

通过下列指令可以查询到数据库的文件格式和行格式配置:

show variables like "InnoDB_file_format";
show variables like "InnoDB_default_row_format";

REDUNDANT和其他几种类型的区别在就是在于首部的内容区别。REDUNDANT的存储格式为首部是一个字段长度偏移列表(每个字段占用的字节长度及其相应的位移),其他类型的存储格式为首部是一个非NULL的变长字段长度列表,这种方式存储数据会更加紧凑(页中存放的行数越多,性能就越高),数据布局如下图:

图片[3]-InnoDB磁盘结构详解-不念博客
数据布局
  • 针对VARCHAR、TEXT、BLOB这类变长字段,列中实际存储了多少数据是不固定的,因此除了要把数据本身存下来,还需要记下它的长度。
  • 如果字段值为NULL,其并不占该部分任何空间,除了占有NULL标志位,故两个字段为NULL就占用2bit。
  • 头信息中包括删除标记、当前记录是否是分组中的最后一条、当前记录在页中的相对位置、记录类型(0:普通记录,1:B+树非叶子节点目录项记录,2:Infimum记录,3:Supremum记录)、下一条记录的相对位置等。
  • 每行数据除了用户定义的列外,还有3个隐藏列,包括trx_id列和roll_pointer列(见下文),分别为6字节和7字节的大小,若表没有定义主键,每行还会增加一个6字节的rowid列。

注意,索引也是按这种方式存储的:

  • 对于聚簇索引,非叶子节点包含主键和child page number,叶子节点包含主键和具体的行;
  • 对于非聚簇索引,也就是二级索引,非叶子节点包含二级索引和child page number,叶子节点包含二级索引和主键值。
© 版权声明
THE END
喜欢就支持一下吧
点赞122赞赏 分享
评论 抢沙发
头像
欢迎光临不念博客,留下您的想法和建议,祝您有愉快的一天~
提交
头像

昵称

取消
昵称代码图片

    暂无评论内容