表空间
磁盘部分包括各种表空间,包括系统表空间(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中的设计中,页与页之间是通过一个双向链表连接起来,而存储在页中的数据行则是通过单链表连接起来的,如下图:
双向链表
页有通用的文件头和尾(将页的内容进行封装,通过文件头和文件尾的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:用户记录,以单链表的形式存储,如下图:
单链表
- 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, general | Antelope or Barracuda |
COMPACT(紧凑) | 是 | 否 | 否 | 否 | system, file-per-table, general | Antelope or Barracuda |
DYNAMIC(动态) | 是 | 是 | 是 | 否 | system, file-per-table, general | Barracuda |
COMPRESSED(压缩) | 是 | 是 | 是 | 是 | file-per-table, general | Barracuda |
通过下列指令可以查询到数据库的文件格式和行格式配置:
show variables like "InnoDB_file_format";
show variables like "InnoDB_default_row_format";
REDUNDANT和其他几种类型的区别在就是在于首部的内容区别。REDUNDANT的存储格式为首部是一个字段长度偏移列表(每个字段占用的字节长度及其相应的位移),其他类型的存储格式为首部是一个非NULL的变长字段长度列表,这种方式存储数据会更加紧凑(页中存放的行数越多,性能就越高),数据布局如下图:
数据布局
- 针对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,叶子节点包含二级索引和主键值。