资料来源:
内容基本与Jeremy Cole的博客内容相同,用于个人整理理解,建议直接阅读以下资料进行学习。
- InnoDB技术内幕
- 掘金小册:从根上了解MySQL
- Jeremy Cole的博客
- 从MySQL InnoDB物理文件格式深入理解索引 - 知乎 (zhihu.com)
表
索引组织表
在InnoDB上,表都是根据主键顺序组织存放的,被称为索引组织表
数据即索引,索引即数据
主键设置:
- 显式定义
- 是否存在非空的唯一索引
- 自动创建一个6字节大小的指针
优先级由上至下
逻辑储存结构
表空间
InnoDB中逻辑储存结构的最高层
表空间是一个抽象的概念,对于系统表空间来说,对应着操作系统层次文件系统中一个或多个实际文件;对于每个独立表空间来说,对应着文件系统中一个名为表名.ibd
的实际文件。
Ibd 文件实际上是一个功能齐全的空间,可以包含多个表,但在 MySQL 的实现中,它们只包含一个表。
在逻辑上,所有数据都被存放在表空间
表空间被划分为许多连续的区,每个区默认由64个页组成,每256个区划分为一组,每个组的最开始的几个页面类型是固定的就好了。
- 系统表空间(system tablespace)。文件以ibdata1、ibdata2等命名,包括元数据数据字典(表、列、索引等)、double write buffer、插入缓冲索引页(change buffer)、系统事务信息(sys_trx)、默认包含undo回滚段(rollback segment)。
- 用户表空间。innodb_file_per_table=true时,一个表对应一个独立的文件,文件以db_name/table_name.ibd命名。行存储在这类文件。另外还有5.7之后引入General Tablespace,可以将多个表放到同一个文件里面。
- redo log。文件以ib_logfile0、ib_logfile1命名,滚动写入。主要满足ACID特性中的Durablity特性,保证数据的可靠性,同时把随机写变为内存写加文件顺序写,提高了MySQL的写吞吐。
- 另外还可能存在临时表空间文件、undo独立表空间等。
表空间格式
表空间(tablespace)有一个32位的spaceid,用户表空间物理上是由page连续构成的,每个page的序号是一个32位的uint,page 0位于文件物理偏移量0处,page 1位于16384偏移量处。由此推出InnoDB单表最大2^32 * 16k = 64T。
表的所有行数据都存在页类型为INDEX的索引页(page)上,为了高效管理表空间,InnoDB以extent为单位申请page使用。
又以256个extent为一段,每段还需要很多其他的辅助页,例如文件管理页FSP_HDR/XDES、插入缓冲IBUF_BITMAP页、INODE页等,用于记录来跟踪所有的页面、区段和表空间本身。
- fsp_hdr:空间中的第一页(0页)总是 fsp_hdr 页。Fsp_hdr 页面包含一个 FSP 标头结构,用于跟踪空间大小以及空闲、片段和完整范围的列表等内容。一个 fsp_hdr 页面内部只有足够的空间存储256个区段(或16,384页,256 MiB)的信息。
- XDES:XDES 和 fsp_hdr 页面的结构是相同的,只是在 XDES 页面中,FSP 头部结构是归零的。
- ibufbitmap:
- INODE:用于存储与文件段相关的列表
后续在page的章节中详细说明
系统表空间
用户表空间
空闲空间管理
段
数据段、索引段、回滚段
索引段即B+树的的叶子节点
对B+树叶子节点中的记录进行顺序扫描,而如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请到的区中的话,进行范围扫描的效果就大打折扣了。
为了将叶子节点数据放在连续的物理结构中,对叶子节点有自己独有的区,非叶子节点也有自己独有的区。存放叶子节点的区的集合就算是一个段(segment
),存放非叶子节点的区的集合也算是一个段。也就是说一个索引会生成2个段,一个叶子节点段,一个非叶子节点段
更高效的页管理:区和组
由连续页组成 任何情况下都为1MB
为保证区中页的连续性,InnoDB储存引擎一次从磁盘申请4~5个区。
一般页的大小为16KB,一个区中由64个连续的页
区的分类:
- 空闲的区:现在还没有用到这个区中的任何页面。
- 有剩余空间的碎片区:表示碎片区中还有可用的页面。
- 没有剩余空间的碎片区:表示碎片区中的所有页面都被使用,没有空闲页面。
- 附属于某个段的区。每一个索引都可以分为叶子节点段和非叶子节点段,除此之外InnoDB还会另外定义一些特殊作用的段,在这些段中的数据量很大时将使用区来作为基本的分配单位。
256个区被划分为一个组
页
是InnoDB管理磁盘的最小单位,也是与内存交互的基本单位
默认16KB,可以通过innodb_page_size
设置为4KB、8KB
- 数据页(B-tree Node)
- undo页
- 系统也
- 事务数据页
- 插入缓冲位图页
- 插入缓冲空闲列表页
- 未压缩的二进制大对象页
- 压缩的二进制大对象页
文件管理页
文件管理页的页类型是FSP_HDR和XDES(extent descriptor),用于分配、管理extent和page。
默认一个extent(1MB大小)管理64个物理连续的page(16k),extent是InnoDB高效分配扩容page的机制。如果page更小(例如8k,4k),则仍然要保证extent最小1M,page数就会相应变多;如果page变大(例如32k),则仍然是64个page。
FSP_HDR/XDES页在表空间中的位置,和内部结构如下。
FSP_HDR页都是page 0,XDES页一般出现在page 16384, 32768等固定的位置。一个FSP_HDR或者XDES页大小同样是16K,容量限制所能管理的extent必定是有限的,一般情况下,每个extent都有一个占40字节的XDES entry描述维护,因此1个FSP_HDR页最多管理256个extent(也就是256M,16384个page)。那么随着表空间文件越来越大,就需要更多的XDES页。
XDES entry存储所管理的extent状态:
- FREE(空)
- FREE_FRAG(至少一个被占用)
- FULL_FRAG(满)
- 归某个segment管理的信息
即上述extent的四种状态
XDES entry还存储了每个extent内部page是否free(有空间)信息(用bitmap表示)。XDES entry组成了一个双向链表,同一种extent状态的收尾连在一起,便于管理。
FSP_HDR和XDES的唯一区别,FSP Header只有在page 0 FSP_HDR中有值。
而FSP Header里面最重要的信息就是四个链表头尾数据(FLST_BASE_NODE结构,FLST意思是first and last),FLST_BASE_NODE如下。
- 当一个Extent中所有page都未被使用时,挂在FSP_FREE list base node上,可以用于随后的分配;
- 有一部分page被写入的extent,挂在FREE_FRAG list base node上;
- 全满的extent,挂在FULL_FRAG list base node上;
- 归属于某个segment时候挂在FSEG list base node上。
当InnoDB写入数据的时候,会从这些链表上分配或者回收extent和page,这些extent也都是在这几个链表上移动的。
INODE页
一般而言,INODE一定会出现在文件的page 2上,如果管理的索引过多,才会分配更多的INODE页。
segment是表空间管理的逻辑单位。INODE页就是用于管理segment的,每个Inode entry负责一个segment。
一个segment由32个碎片页(fragment array),FSEG_FREE、FSEG_NOT_FULL、FSEG_FULL组成,这些信息记录在Inode entry里,可以简单理解为Inode就是segment元信息的载体。
FREE、NOT_FULL、FULL三个FLST_BASE_NODE对象和FSP_HDR/XDES页里面的FSP_FREE、FREE_FRAG、FULL_FRAG、FSEG概念类似。这些链表被InnoDB使用,用于高效的管理页分配和回收。
INDEX数据索引页
每个索引页面的总体结构如下:
- File Header 页的通用信息
- Page Header 数据页专有信息
- infimun 、Supermun Records 最大记录和最小记录
- User Records 用户记录
- Free Space 空闲空间
- Page Directory 页目录
- File Trailer 文件尾部 校验是否完整
业内数据的组织方式
小结
行记录格式
Compact
由可选的两个标识+record header+body组成,具体如下。
除了记录实际数据外,还有描述这行信息的额外信息
- 变长字段
- NULL值列表
- 记录头信息
变长字段长度列表
Variable field lengths: 可选标识,变长字段长度,如果没有变长字段,就不存在。每个变长字段都用1-2个字节表示长度,根据列定义顺序逆序存放,如果小于等于127,则1个字节;如果小于等于127,则1个字节;大于127,低字节下一位的表示是否有overflow page存储,剩余6位和高字节的8位,按照大尾端encoding组成变长长度。
null值标志位
记录行内数据 为null值的位置,真实数据中不会保存null值
先统计允许储存null的列
Nullable field bitmap:可选标识,表明哪些列是NULL,如果没有nullable字段,就不存在。一个字节能表示8个nullable字段,超过8个字段就扩充到低字节。如下图所示,18个字段,9个可为空,如果其中某3个实际为空,则两个字节存储如图。
记录头信息
record header: 固定5个字节长度。
Info Flags:1个字节。低4位表示是否min_rec或者deleted。高4位表示num of records owned,与上面提到的page directory呼应,如果被page directory slot指向,则有值。
2个大尾端字节:低三位表示类型, 包括普通记录REC_STATUS_ORDINARY=0,非叶子节点记录REC_STATUS_NODE_PTR=1,起始虚拟记录REC_STATUS_INFIMUM=2,终点虚拟记录REC_STATUS_SUPREMUM=3。高5位表示heap no,即顺序位置。
2字节next record offset: 直接定位到下一个record的数据部分,也就是主键偏移量,而不是record header。
可以看出如果表结构没有变长字段,没有nullable字段,则不会存在冗余信息。5个字节长度的record header是必须有的,上面提到的infimum和supremum也是一种特殊的row,只不多对用户不可见。
隐藏列
t_id、roll_pointer这两个值用来支持MVCC机制,事务ID是实现事务隔离级别的基础,而通过回滚指针指向undo log,可实现非锁定一致性读。
数据列
索引
序列化后存储于此,例如int类型索引主键就占用4个字节。
对于聚簇索引的叶子节点,存储行。
对于二级索引的叶子节点,存储行的主键值。
对于聚簇索引和二级索引的非叶子节点,存储child page最小的key。
上面提到的infimum和supremum中就只存字符串在行数据里。
非主键列的数据
对于聚簇索引的叶子节点,是按照表结构定义排列的columns,每种column类型都有自己的encoding方法。
对于二级索引的叶子节点,是行的主键值。
对于聚簇索引和二级索引的非叶子节点,是child page number。