MySQL InnoDB存储引擎表的逻辑存储结构及实现原理详细介绍
文章目录
- 一、表空间(Tablespace)
- 1. 系统表空间(System Tablespace)
- 2. 独立表空间(File-Per-Table Tablespace)
- 3. 通用表空间(General Tablespace)
- 4. 临时表空间(Temporary Tablespace)
- 5. 回滚表空间(Undo Log Tablespace)
- 二、段(Segment)
- 三、区(Extent)
- 四、页(Page)
- 1. 常见页类型
- 2. 数据页结构
- (1) File Header(文件头,38字节)
- (2) Page Header(页头,56字节)
- (3) Infimum+Supremum(最小/最大记录,26字节)
- (4) User Records(用户记录,动态大小)
- (5) Free Space(空闲空间,动态大小)
- (6) Page Directory(页目录,动态大小)
- (7) File Trailer(文件尾,8字节)
- 3. 页的存储和使用
- (1) 行记录链表
- (2) 槽(Slot)分组
- (3) 页目录(Page Directory)的构建
- (4) 二分法查询
- 4. 页的管理机制
- (1) 页的分配与回收
- (2) 页内行记录的删除机制
- (3) 页的刷新机制
- (4) 页的分裂与合并
- 5. Named File Format机制
- 五、行(Row)
- 1. 行格式类型
- (1) Compact行记格式
- (2) Redundant行格式
- (3) 溢出行数据
- (4) Compressed和Dynamic行格式
- 2. CHAR的行结构存储
- 3. 行记录的删除机制
- 4. 行存储与聚簇索引
MySQL的存储引擎是基于表的,而不是数据库的。作为MySQL的核心存储引擎,InnoDB存储引擎的逻辑存储结构采用分层架构设计,他们从高到低依次为表空间→段→区→页→行,各层级协同工作以实现高效的数据存储与访问。InnoDB存储引擎的逻辑存储结构大致如下图所示:
一、表空间(Tablespace)
InnoDB的所有数据都被逻辑地存放在一个空间中,称之为表空间(tablespace)。表空间由多种不同的段(segment)组成。
InnoDB表空间分类是MySQL数据库存储管理的核心架构,包含系统表空间、独立表空间、通用表空间、临时表空间、回滚表空间五大核心类型,主要存储表数据、索引和事务信息等。
1. 系统表空间(System Tablespace)
- 核心作用:存储元数据(表/索引/列定义)、双写缓冲区、回滚日志、变更缓冲、系统事务数据等全局数据,是InnoDB的默认共享存储区。
- 文件结构:默认文件为
ibdata1
,可扩展为多个文件(如ibdata1:100M;ibdata2:200M:autoextend
),支持裸设备分区提升I/O效率。MySQL 8.0后,数据字典不再保存在系统表空间,但双写缓冲、插入缓冲仍存储于此。 - 配置参数:
innodb_data_file_path
:定义文件路径、大小及自动扩展策略(如末文件可扩展至最大1GB)。innodb_data_home_dir
:指定表空间目录(默认datadir
)。
- 版本差异:MySQL 8.0前用户表数据可能存储于此,之后默认启用独立表空间;双写缓冲区(16KB页)防止部分写入失效,确保数据页可靠性。
- 管理要点:碎片化空间无法自动收缩,需通过重建表空间回收;高并发下可能成I/O瓶颈。
2. 独立表空间(File-Per-Table Tablespace)
- 核心作用:每张表独占一个
.ibd
文件,存储表数据、索引及元数据,实现表级隔离。 - 配置逻辑:
- 默认启用(
innodb_file_per_table=ON
),新表自动创建独立文件;支持DATA DIRECTORY
子句指定存储路径(如外部磁盘)。 - 支持数据在线迁移(表空间传输功能),需满足版本一致、表结构相同等条件。
- 默认启用(
- 优势:
- 空间回收:
TRUNCATE TABLE
或ALTER TABLE ... ENGINE=InnoDB
释放空间至文件系统。 - 迁移灵活:可复制
.ibd
文件至其他实例,通过ALTER TABLE ... IMPORT TABLESPACE
导入。 - 并发优化:多表写入分散至不同文件,减少共享表空间I/O竞争。
- 空间回收:
- 限制:只存储了表的索引和数据,但是其他类的数据,如回滚(undo)信息,变更缓冲、索引页、事务信息,二次写缓冲(Double write buffer)等全局数据还是存放在系统表空间内。
3. 通用表空间(General Tablespace)
- 核心作用:跨数据库共享存储,允许多个表存储于同一
.ibd
文件,减少元数据内存开销。 - 创建方式:
- 语法:
CREATE TABLESPACE ts ADD DATAFILE '/path/ts.ibd' FILE_BLOCK_SIZE=16K ENGINE=InnoDB
。 - 支持自定义路径(需通过
innodb_directories
配置目录),避免与独立表空间冲突。
- 语法:
- 优势:
- 性能优化:减少
DROP/TRUNCATE
的文件系统开销,提升大批量操作效率。 - 存储管理:支持表压缩(需统一压缩格式),减少存储占用。
- 性能优化:减少
- 管理要点:删除表空间前需先删除所有表;通过
ALTER TABLE ... TABLESPACE=
转移表。
4. 临时表空间(Temporary Tablespace)
- 核心作用:存储临时表、排序数据、哈希连接中间结果等临时数据,避免占用永久存储。
- 文件结构:默认文件为
ibtmp1
(12MB自动扩展),位于数据目录,服务器关闭时自动删除并重建。 - 配置参数:
innodb_temp_data_file_path
控制路径、大小及扩展策略(如ibtmp1:12M:autoextend:max:1G
)。 - 应用场景:支持
ORDER BY
、GROUP BY
、索引创建等操作的临时存储,提升查询效率。 - 监控:通过
INFORMATION_SCHEMA.INNODB_TEMP_TABLESPACES
查看使用情况。
5. 回滚表空间(Undo Log Tablespace)
- 核心作用:存储撤销日志,支撑事务回滚、MVCC(多版本并发控制)及崩溃恢复。
- 存储逻辑:
- MySQL 8.0后默认独立于系统表空间,文件名为
undo_001
、undo_002
等,支持动态添加(CREATE UNDO TABLESPACE ... ADD DATAFILE
)。 - 每个表空间支持128个回滚段(
innodb_rollback_segments
),通过innodb_undo_tablespaces
控制数量。
- MySQL 8.0后默认独立于系统表空间,文件名为
- 配置参数:
innodb_undo_directory
:指定存储目录(默认datadir
)。innodb_max_undo_log_size
:设置截断阈值(默认1GB),超量时自动回收空间。
- 管理要点:监控工具
INNODB_METRICS
查看截断状态,SET GLOBAL innodb_monitor_enable=module_undo
启用日志。
二、段(Segment)
段是表空间的逻辑分区,按数据类型管理存储。每个段由多个区(Extent)组成。
-
常见分类:
- 数据段(Leaf node segment):叶子段,B+树叶子节点,存储实际行数据(如聚簇索引的完整行)。
- 索引段(Non-Leaf node segment):非叶子段,B+树非叶子节点,存储索引键值及指针。
- 回滚段(Rollback Segment):存储事务修改前的旧值(Undo log),支持回滚与MVCC。
- 变更缓冲段(Change Buffer):缓存非唯一索引的DML操作,合并写入磁盘以减少随机IO。
-
段的分配机制:
每个表至少会分配一个主键索引,一个索引会对应两个段(叶子段+非叶子段),每一个段由多个区组成,而段是以区为单位申请存储空间的,一个区默认占用1M(64*16Kb=1024Kb)存储空间,当启用独立表空间(innodb_file_per_table=ON
)时,每个表就会至少分配2M的空间。那么问题来了,当小表或undo这类的段,在初始时候也去分配2M的空间,就会造成磁盘空间的浪费。对此,InnoDB存储引擎提出了碎片页(fragment page)的概念,以此来优化对段空间的分配使用:- 在刚开始向表中插入数据的时候,段会优先申请使用碎片页来保存数据,由数据大小本身决定自动扩展,直到占满32个碎片页。
- 当某个段已经占用了32个碎片页之后,就会申请以完整的区(64个连续页)为单位来分配存储空间。
注意:段在严格意义上来讲既可以是由多个碎片页组成,又可以是由多个完整的区组成。
三、区(Extent)
区是由连续的页组成的空间,默认1MB(64个16KB页)。为了保证区中页的连续性,innodb一次性从磁盘申请4-5个区,提高I/O效率,减少碎片化。
核心特性
- 连续性:确保页的物理连续,提升顺序读写效率(如预读、范围查询)。
- 分配策略:段增长时按需分配区(通常4-5个区),避免碎片化。小表初始使用碎片页存储,超32个碎片页后分配完整区。
- 类型:统一区(同类型页)与混合区(多类型页,用于小对象存储)。
四、页(Page)
页也可以称为块(Block),是磁盘和内存之间交互的最小单位,默认16KB,可通过 innodb_page_size
配置为4K/8K/16K
(数据库初始化之后不可修改)。每个页包含多个数据行(记录),最少2个数据行。
1. 常见页类型
- 数据页(Data Page):存储实际数据行(用户记录)。
- 索引页(Index Page):存储B+树索引结构(叶子节点和非叶子节点)。
- 系统页(System Page):存储元数据(如数据字典、双写缓冲等)。
- 事务数据页(Transaction system Page):用于记录事务状态信息。
- Change Buffer页:存储变更缓冲区的数据。
- Undo页:存储回滚日志(Undo log)数据。
- Blob页:存储大字段(如VARCHAR、BLOB、TEXT)的溢出行数据。
2. 数据页结构
(1) File Header(文件头,38字节)
- 核心作用:标识页的元数据与完整性。
- 关键字段:
FIL_PAGE_SPACE_OR_CHKSUM
:4字节校验和,与File Trailer协同验证页完整性。FIL_PAGE_OFFSET
:4字节页号,定位表空间中的物理位置。FIL_PAGE_PREV/NEXT
:双向链表指针,连接相邻页(如B+树节点)。FIL_PAGE_TYPE
:2字节标识页类型(如0x45BF为数据页,0x0002为Undo日志页)。FIL_PAGE_LSN
:8字节记录最近修改的日志序列号,用于事务恢复。
(2) Page Header(页头,56字节)
- 核心作用:管理页内状态与记录分布。
- 关键字段:
PAGE_N_RECS
:2字节记录用户记录总数。PAGE_HEAP_TOP
:2字节指向空闲空间起始位置。PAGE_FREE
:2字节指向空闲链表头,用于空间回收。PAGE_N_DIR_SLOTS
:2字节页目录槽数量,支持二分查找。PAGE_LEVEL
:2字节表示B+树层级(叶节点为0)。
(3) Infimum+Supremum(最小/最大记录,26字节)
- 系统生成记录:作为虚拟边界,确保记录按主键有序排列。
Infimum
:最小记录(负无穷),指向主键最小的用户记录。Supremum
:最大记录(正无穷),主键最大的用户记录指向它。
- 结构:包含记录类型标识、下一条记录偏移量及固定内容(如"infimum"字符串)。
(4) User Records(用户记录,动态大小)
- 存储内容:实际数据行,采用紧凑行格式(Compact/Dynamic/Compressed)。
- 记录结构:
- 变长字段长度列表:逆序存储VARCHAR/TEXT等字段长度。
- NULL标志位:标记可为NULL的列。
- 记录头信息(5字节):
delete_flag
:标记记录是否被删除。heap_no
:记录在页中的相对位置(从2开始,0和1是Infimum和Supremum)。record_type
:记录类型(0=普通记录,1=B+树非叶子节点,2=最小记录,3=最大记录)。next_record
:当前记录到下一条记录的地址偏移量。
- 隐藏列:事务ID(6字节)、回滚指针(7字节),支撑MVCC与事务。
- 列值:实际存储的数据。
- 行溢出处理:Dynamic格式将大字段(如BLOB)存储在溢出页,本页仅保留20字节指针。
(5) Free Space(空闲空间,动态大小)
- 作用:为新记录或更新分配空间,支持动态空间管理。
- 机制:
- 插入时从空闲空间分配,删除记录后加入空闲链表供重用。
- 采用首次适应/最佳适应算法优化空间分配,减少碎片。
(6) Page Directory(页目录,动态大小)
- 核心作用:加速记录查找,通过槽(Slot)分组记录。
- 结构:
- 槽指向每组最大记录的偏移量,每组4-8条记录(最小记录组仅1条)。
- 支持二分查找(时间复杂度O(log n)),例如6条记录分2组,槽指向最小与最大记录。
- 查找流程:通过槽定位组,再遍历组内记录链表。
(7) File Trailer(文件尾,8字节)
- 核心作用:验证页完整性,防止写入中断导致数据损坏。
- 内容:校验和(与File Header的校验和匹配),确保磁盘写入一致性。
3. 页的存储和使用
(1) 行记录链表
- 行记录按照主键大小排序,形成一个链表
- 链表从
Infimum
开始,到Supremum
结束 - 每条记录通过
next_record
指针连接到下一条记录 - 链表顺序:按照主键值从小到大排序
(2) 槽(Slot)分组
- 所有记录(包括Infimum和Supremum,不包括被删除的记录)被划分为几个组,每个组就表示为一个槽(Slot)。
- 分组规则:
- 最小记录(Infimum)所在的分组:1条
- 最大记录(Supremum)所在的分组:1~8条
- 其他分组:4~8条
(3) 页目录(Page Directory)的构建
- 每个组的最后一条记录(即主键值最大的记录)的头信息中,
n_owned
属性记录该组的记录数 - 将每个组最后一条记录的地址偏移量提取出来,按顺序存储到页目录
- 页目录中的这些偏移量称为槽(Slot)
(4) 二分法查询
- 首先是从B+树的根开始,逐层检索,直到找到叶子节点,也就是找到对应的数据⻚为止;
- 然后将数据⻚加载到内存中,⻚目录中的槽(slot)采用二分查找的方式确定记录所在的槽;
- 接着找到槽所在分组中主键值最小的记录;
- 最后在该槽分组中通过
next_record
属性从最小值行记录开始遍历链表的方式找到记录。
4. 页的管理机制
(1) 页的分配与回收
- InnoDB使用**区(Extent)**作为分配单位,1个区=64个连续页(1MB)
- 每次分配时至少申请4-5个区
- 空闲空间(Free Space)被回收利用,减少碎片化
(2) 页内行记录的删除机制
- 当行记录被删除时:
delete_flag
被标记为1next_record
变为0- 上一条记录的
next_record
相应更新
- 记录不会立即从磁盘上移除,而是组成垃圾链表,占用的空间称为可重用空间
- 这样设计可以避免频繁的磁盘I/O操作
(3) 页的刷新机制
- 当页被修改后,会成为脏页(Dirty Page)
- Master Thread定期将脏页刷新到磁盘
- 刷新策略由
innodb_max_dirty_pages_pct
和innodb_io_capacity
参数控制
(4) 页的分裂与合并
前面说过数据页有固定的大小(通常为 16KB)。当页中的行数据达到存储极限时,InnoDB 会触发页分裂机制,将数据分散到新的页中。当页中的数据量减少到一定程度(例如小于 50%)时,InnoDB 会尝试合并当前页与相邻页,以减少存储碎片并优化查询效率。
页分裂的触发条件:
- 主键插入顺序:当新插入的记录超过页的容量时,触发页分裂。
- 非主键索引:对索引页的插入可能导致分裂,尤其是当索引值分布不均时。
页分裂的过程:
- 新页分配:从当前段中分配一个空闲页,用作分裂后的目标页。
- 数据迁移:将当前页中约一半的数据迁移至新页。
- 父节点更新:更新上层索引页中的指针,记录新页位置。
性能影响:
- 页分裂会导致树的深度增加,从而增加查询路径。
- 页合并会减少树的深度,但在高并发写入时可能引发锁争用。
- 频繁的分裂和合并可能导致存储碎片增多,从而降低存储利用率和检索性能。通过定期优化表(OPTIMIZE TABLE)可以缓解这一问题。
5. Named File Format机制
随着InnoDB存储引擎的发展,为了扩展支持新版本的特性,从InnoDB1.0.x版本开始,通过引入Named File Formats机制来解决不同版本下页结构兼容性的问题。
InnoDB存储引擎将1.0.x版本之前的文件格式(File Format)定义为Antelope,将新版本支持的文件格式定义为Barracuda,可以看出新的页格式包含旧版本的全部页格式。查看源码可以发现InnoDB定义了一个按首字母排序的动物名字(A~Z)的数组,可能后续会继续升级扩展。
相关参数说明:
innodb_file_format
:用来指定页文件格式,可以通过SHOW VARIABLES LIKE '%innodb_file_format%;
命令来查看当前所使用的InnoDB存储擎的文件格式。innodb_file_format_check
:用来检测当前InnoDB存储引擎文件格式的支持度,该值默认为ON,如果出现不支持的文件格式,会在错误日志文件中抛出错误提示。
五、行(Row)
InnoDB的记录是以行的形式存储的,行结构通过紧凑的物理存储、隐式系统字段(如事务ID、回滚指针)及页内行槽优化,实现了高效的数据存储、事务支持和并发控制。
1. 行格式类型
InnoDB支持多种行记录格式,每种行格式在存储效率、性能上有所不同:
行格式类型 | 引入版本 | 特点 |
---|---|---|
Compact | MySQL 5.0 | 比REDUNDANT更紧凑,减少存储空间(如NULL值不占额外空间),但仍有变长字段长度前缀 |
Redundant | InnoDB原始格式 | 存储固定长度字段和可变长度字段(如VARCHAR)时效率较低 |
Dynamic | MySQL 5.1 | 在MySQL 5.7+默认,支持大字段(如TEXT/BLOB)的“页外存储”(Off-Page),仅保留20字节指针在行内,避免行数据跨页 |
Compressed | MySQL 5.1 | 在Dynamic基础上增加对整页进行压缩,减少存储空间但增加CPU开销 |
(1) Compact行记格式
Compact行记录是在MySQL5.0中引入的,其设计目标是高效地存储数据。简单来说一个页中存放的行数据越多,其性能就越高。下图显示了Compact行记录的存储方式:
- 变长字段长度列表:按照列的顺序逆序记录每一个列实际数据的长度,如果列的长度小于 255 字节,则使用一个字节,否则使用 2 个字节。
- NULL标志位:占用1字节,表示该行数据中是否存在有NULL值的列,有则用1表示。
- 记录头信息:占用5字节(40位),记录了该行属性相关的信息。
- 预留位1:占1位,未使用,预留位。
- 预留位2:占1位,未使用,预留位。
- delete_flag:占1位,表示该行是否已被删除。
- min_rec_flag:占1位,值为1,表示该行被定义为B+树的每层非叶子节点中最小的记录。
- n_owned:占4位,表示该行在当前页中槽分组的记录数。
- heap_no:占13位,表示该行在当前页中的索引位置。
- record_type:占3位,表示该行的类型,000表示普通,001表示B+树节点指针,010表示Infimum,011表示Supremum,1xx表示保留。
- next_record:占16位,表示指向页记录链表下一条记录的相对位置。
- 列数据信息:
- 自定义列(table column):保存了用户表结构定义的全部列对应的真实数据。
- 行ID(rowid):占6字节,隐藏列,不一定存在。仅当表没有定义主键,且该表也没有定义唯一索引,此时InnoDB会给每个行生成一个rowid作为B+树索引的排序编号。
- 事务ID(trx_id):占6字节,隐藏列,一定存在。保存了最近修改该行的事务 ID(MVCC 可见性判断用)。
- 回滚指针(rollback_pointer):占7字节,隐藏列。一定存在,保存的是回滚指针,指向 undo log 中的历史版本。
(2) Redundant行格式
Redundant是MySQL5.0版本之前InnoDB的行记录存储方式,MySQL5.0支持 Redundant是为了兼容之前版本的页格式。下图显示了Redundant行记录的存储方式:
- 字段长度偏移列表:按照列的顺序逆序记录每一个列实际数据的长度的偏移量,即列存储的数据的实际长度为前后两个列的偏移量相减。如果列的长度小于 255 字节,则使用一个字节,否则使用 2 个字节。
- 记录头信息:占用6字节(48位),记录了该行属性相关的信息。
- 预留位1:占1位,未使用,预留位。
- 预留位2:占1位,未使用,预留位。
- delete_flag:占1位,表示该行是否已被删除。
- min_rec_flag:占1位,值为1,表示该行被定义为B+树的每层非叶子节点中最小的记录。
- n_owned:占4位,表示该行在当前页中槽分组的记录数。
- heap_no:占13位,表示该行在当前页中的索引位置。
n_fields
:占10位,表示该行记录的列的数量。最大值为1023,也就说明MySQL最大支持1023个列。1byte_offs_flag
:占1位,表示了字段长度偏移列表是占1字节还是2字节。- next_record:占16位,表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。
- 列数据信息:
- 自定义列(table column):保存了用户表结构定义的全部列对应的真实数据。
- 行ID(rowid):占6字节,隐藏列,不一定存在。仅当表没有定义主键,且该表也没有定义唯一索引,此时InnoDB会给每个行生成一个rowid作为B+树索引的排序编号。
- 事务ID(trx_id):占6字节,隐藏列,一定存在。保存了最近修改该行的事务 ID(MVCC 可见性判断用)。
- 回滚指针(rollback_pointer):占7字节,隐藏列。一定存在,保存的是回滚指针,指向 undo log 中的历史版本。
(3) 溢出行数据
在了解行溢出前我们先了解一下MySQL InnoDB支持的VARCHAR类型长度问题:
- 官方公布的VARCHAR最大支持长度为
65535
字节,这个65535
字节长度是表的全部列字段的字节长度之和。 - 在多字节字符编码下(如gbk/utf-8)最大只能支持
65532
字节。 - 在多字节字符编码下(如gbk/utf-8)建表语句中的VARCHAR(N)中的N表示的是N个字符长度,非字节长度。
问题来了,前面提到的页数据大小最大为16K,即16384
字节。那么65532
字节是怎么存放的呢?
答案是:行溢出方式保存。在一般情况下,InnoDB的页数据都存放在页类型为B-tree Node中,当行数据超过8098
字节时,就会发生行溢出,此时行溢出数据就会存放在Uncompressed Blob Page中。如下图,行数据只存储了前768
字节的前缀(prefix)数据,剩余的行溢出偏移量数据指向行溢出页,也就是Uncompressed BLOB Page。
(4) Compressed和Dynamic行格式
InnoDB1.0.x版本开始引入了新的文件格式:Barracuda文件格式。Barracuda文件格式除了包含原有的Antelope文件格式(Compact和Redundant格式),同时新增了两种行记录格式:Compressed和Dynamic。
新的两种记录格式对于存放在BLOB中的数据采用了完全的行溢出的方式,如下图所示,在数据页中只存放20个字节的指针,实际的数据都存放在Off Page中,而之前的Compact和Redundant两种格式会存放768个前缀字节。
Compressed行记录格式的另一个功能就是,存储在其中的行数据会以zlib的算法进行压缩,因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据能够进行非常有效的存储。
2. CHAR的行结构存储
通常理解VARCHAR是存储变长长度的字符类型,CHAR是存储固定长度的字符类型。而在前面已经了解行结构的内部的存储,可以发现每行的变长字段长度的列表都没有存储CHAR类型的长度。
从 MySQL4.1版本开始,CHAR(N)中的N指的是字符的长度,而不是字节长度。也就说在不同的字符集下,CHAR类型列内部存储的可能不是定长的数据。
例如,对于 UTF-8下CHAR(10)类型的列,其最小可以存储10字节的字符,而最大可以存储 30字节的字符。因此,对于多字节字符编码的CHAR数据类型的存储,InnoDB存储引擎在内部将其视为变长字符类型,对于未能占满长度的字符则会填充0x20直到填满。
所以我们可以认为在多字节字符集的情况下,CHAR和VARCHAR的实际行存储基本是没有区别的。
3. 行记录的删除机制
InnoDB的行删除是先逻辑删除,后物理删除:
- 标记删除:将
deleted_flag
设为1 - 不立即回收空间:记录被放入垃圾链表(Garbage Chain)
- 空间重用:当有新记录插入时,会重用这些空间
- 物理删除:由后台线程(Page Cleaner Thread)定期清理
4. 行存储与聚簇索引
InnoDB是索引组织表(IOT),数据按聚簇索引顺序存储:
- 主键存储:行数据直接包含主键列(或隐式ROW_ID),聚簇索引的叶子节点存储完整行数据。
- 二级索引存储:非聚簇索引(如普通索引)的叶子节点存储主键值(或ROW_ID),通过回表(Bookmark Lookup)获取完整行数据。