MySQL InnoDB记录存储结构深度解析
1. InnoDB存储引擎概述
InnoDB是MySQL默认的事务型存储引擎,广泛用于OLTP(在线事务处理)场景。它的设计目标是保证数据完整性、事务安全性和高并发性能。理解InnoDB的记录存储结构,必须先掌握其宏观架构和核心机制。
1.1 存储架构与页(Page)概念
InnoDB的存储结构可以分为以下几个层次:
表空间(Tablespace)
数据物理存储单元,可以是共享表空间(ibdata1)或独立表空间(.ibd文件)。
表空间中存储的数据被划分为页(Page),页是InnoDB最小的I/O单位。
默认页大小为16KB,可通过
innodb_page_size
调整。
页(Page)
每个页类似于硬盘上的“集装箱”,里面存储行记录、页目录、页头信息等。
页类型主要有:
数据页(Index Page):存储B+树叶子节点记录。
非叶子节点页(Interior Page):存储B+树索引信息(键值+指针)。
BLOB页(Overflow Page):存储大字段的溢出数据。
Undo页:存储事务回滚信息。
行记录(Row)
页中的基本存储单元,每行由固定长度字段 + 变长字段 + NULL标志位组成。
InnoDB支持不同行格式(REDUNDANT、COMPACT、DYNAMIC、COMPRESSED),每种格式在页中的存储布局不同。
⚡ 类比说明:页就像“集装箱”,行记录是集装箱里的“箱子”,字段则是箱子里的具体物品。页目录相当于箱子编号,方便快速查找。
1.2 事务支持与ACID特性
InnoDB支持完整的ACID事务特性:
Atomicity(原子性):事务操作要么全部完成,要么全部回滚。
Consistency(一致性):事务开始和结束时,数据库必须处于一致状态。
Isolation(隔离性):通过多版本并发控制(MVCC)保证事务间隔离。
Durability(持久性):事务提交后数据通过Redo日志保证永久保存。
关键机制:
Undo日志
用于回滚事务和实现MVCC。
Undo信息通常存储在专用Undo页中,并通过聚簇索引维护回滚链表。
Redo日志
用于崩溃恢复,保证已提交事务的数据持久性。
Redo日志记录物理页的修改操作,而非行级别逻辑变更。
锁机制
支持行锁(Record Lock)、间隙锁(Gap Lock)、意向锁(Intention Lock)等。
行锁粒度小,支持高并发写操作。
1.3 聚簇索引与辅助索引
InnoDB表使用**聚簇索引(Clustered Index)**组织表数据:
聚簇索引叶子节点存储完整行数据,主键顺序决定行存储顺序。
辅助索引(Secondary Index)的叶子节点只存储主键值 + 索引列。
查询时:
使用主键直接定位页和行。
使用辅助索引需要先通过索引查到主键,再回聚簇索引查数据。
⚡ 类比说明:聚簇索引是“以主键为地址的档案柜”,辅助索引是“目录索引表”,通过目录找到主柜位置。
1.4 数据一致性与MVCC原理
InnoDB通过**多版本并发控制(MVCC)**实现一致性读:
每行记录包含两个隐藏列:
DB_TRX_ID
:最近修改该行的事务ID。DB_ROLL_PTR
:指向Undo日志链表的指针。
查询操作通过比较事务ID和Undo日志,生成快照读,保证读取一致性。
⚡ 类比说明:MVCC就像“时间机器”,可以让事务看到历史版本的数据而不阻塞其他事务。
1.5 配置与性能优化提示
innodb_file_per_table
开启独立表空间可以减少碎片化,便于表级恢复和压缩。
innodb_page_size
对大数据量表,可将页大小调整为32KB或64KB,提高顺序扫描效率。
事务日志配置
Redo日志文件大小和缓冲池大小应根据负载优化,以减少磁盘I/O。
💡 小结
本章梳理了InnoDB的宏观存储架构和核心概念:表空间 → 页 → 行 → 字段,以及事务、MVCC、索引对存储和查询的影响。理解这些基础,对于后续深入分析页结构和行格式至关重要。
2. 页结构详解
InnoDB中,**页(Page)**是最小的存储与I/O单位,每个页默认大小为16KB(可通过innodb_page_size
修改)。页不仅存储数据,还维护索引信息、事务日志指针等元数据。理解页结构,是分析行格式和性能优化的前提。
2.1 页的分类与作用
InnoDB页按功能可分为以下几类:
数据页(Index Leaf Page)
存储B+树叶子节点上的完整行数据。
聚簇索引的叶子节点即为数据页。
非叶子节点页(Index Internal Page)
存储索引键和子页指针,不包含完整行数据。
用于B+树导航,快速定位数据页。
Undo页(Undo Log Page)
存储事务回滚信息,用于回滚事务和MVCC版本管理。
BLOB/Overflow页(Overflow Page)
存储大字段(如VARCHAR、TEXT、BLOB)超过768字节的数据。
主数据页仅存储指针,实际数据存放在BLOB页。
系统页(System Page)
包括FSP header、insert buffer bitmap等,维护表空间元信息。
⚡ 类比说明:页就像“硬盘上的小房间”,每类页承担不同功能:有的存数据,有的存索引,有的存历史记录,有的存大对象。
2.2 页头/页尾字段解析
每个页都包含页头(File Header)和页尾(File Trailer),用于维护页的状态和完整性。
2.2.1 页头(Page Header)
页头通常位于页开头,占用约38~56字节,主要字段包括:
字段名 | 大小 | 作用 |
---|---|---|
PAGE_N_DIR_SLOTS | 2B | 页目录槽数 |
PAGE_N_HEAP | 2B | 页中记录数 |
PAGE_FREE | 2B | 空闲空间指针 |
PAGE_LSN | 8B | 最新修改日志序列号(Log Sequence Number) |
PAGE_PREV /PAGE_NEXT | 4B各 | 链接同类页形成双向链表 |
PAGE_TYPE | 2B | 页类型标识(数据页/索引页/Undo页等) |
⚡ LSN(Log Sequence Number)用于Crash Recovery,标记页的最新修改状态。
2.2.2 页尾(Page Trailer)
页尾用于完整性校验,主要字段包括:
字段名 | 大小 | 作用 |
---|---|---|
PAGE_FILE_FLUSH_LSN | 8B | 页写磁盘的日志序列号 |
PAGE_SPACE_ID | 4B | 所属表空间ID |
校验码(Checksum) | 4B | 数据完整性验证 |
⚡ 类比说明:页头像“房间门口的登记簿”,页尾像“安全锁”,保证房间被正确使用。
2.3 用户记录存储机制
页中的行记录存储有严格布局,主要组成部分:
记录头(Record Header)
包含前向/后向指针、标记位、NULL位信息。
COMPACT格式记录头为5字节,REDUNDANT为7~9字节。
字段数据
固定长度字段:按声明顺序存储,长度固定。
变长字段:存储长度+数据,COMPACT格式采用逆序存储长度列表。
NULL标志位:用bit位表示哪些字段为NULL。
页目录(Page Directory)
页内有一个槽位数组,指向记录链表的起始位置。
插入、删除、更新操作通过页目录快速定位记录。
空闲空间管理
PAGE_FREE
指向页中可用空间的起始位置。插入新记录时,InnoDB先检查连续空间,再考虑碎片整理。
2.3.1 记录链表伪代码
// 遍历页内记录链表
Record* rec = page->first_record;
while (rec != NULL) {process_record(rec); // 处理记录rec = rec->next_record; // 通过next_record指针遍历
}
⚡ 注释:
first_record
指向页内第一个记录,next_record
为记录头中的偏移字段,形成单向链表,页目录用于加速定位。
2.4 页分裂与碎片化
当页插入新记录后空间不足,会触发页分裂(Page Split):
将页拆成两个页,部分记录移动到新页。
B+树非叶子节点需要调整索引指针。
高并发写入和大字段更新会导致页碎片化,降低存储密度和查询性能。
优化方法:定期使用
OPTIMIZE TABLE
整理碎片,或者选择DYNAMIC/COMPRESSED行格式存储大字段。
💡 小结
本章重点解析了InnoDB页的内部结构:
页的分类和功能:数据页、索引页、Undo页、BLOB页。
页头/页尾字段:LSN、页类型、校验码保证完整性。
用户记录布局:记录头、字段数据、页目录、链表指针。
页分裂与碎片化:影响存储密度和查询性能,需优化。
理解页结构后,才能深入分析行格式的存储细节,如COMPACT、REDUNDANT、DYNAMIC对页内记录的布局差异。
3. 行格式深度解析
InnoDB页中的每条记录都按照**行格式(Row Format)**存储。行格式决定了记录头长度、变长字段存储方式、NULL标志位位置、溢出列处理策略等,直接影响存储密度和查询性能。
3.1 COMPACT行格式
COMPACT是MySQL 5.0之后默认行格式,相比REDUNDANT存储更加紧凑,提高页利用率。
3.1.1 记录头与字段存储
记录头:5字节
info_bits
:1字节,标记删除/外部列等状态n_owned
:1字节,继承自REDUNDANT格式的遗留标志next_record
:2字节,指向下一条记录偏移heap_no
:1字节,记录在页目录中的槽位编号
固定长度字段:顺序存储,无额外长度信息
变长字段:
长度列表:逆序存储在记录尾部,每个字段长度占1或2字节
NULL标志位:独立存储在记录尾部,每个字段占1 bit
⚡ 类比说明:固定字段像“箱子里的固定格子”,变长字段像“伸缩格子”,通过尾部长度列表快速定位数据。
3.1.2 SQL建表示例
CREATE TABLE t_compact (id INT NOT NULL,name VARCHAR(100),description TEXT,created_at DATETIME,PRIMARY KEY (id)
) ROW_FORMAT=COMPACT;
3.1.3 COMPACT格式伪代码解析
// 解析COMPACT行记录
Record* rec = page->first_record;
while (rec != NULL) {int fixed_offset = rec->start; // 固定字段起始位置process_fixed_fields(rec, fixed_offset);int var_offset = rec->end - var_len_list_size; // 变长字段从尾部倒序解析process_var_fields(rec, var_offset, var_len_list);rec = rec->next_record;
}
3.1.4 性能特点
优点:页利用率高,查询效率较好
缺点:大字段仍可能导致BLOB溢出,需要Off Page存储
3.2 REDUNDANT行格式
REDUNDANT是早期MySQL默认行格式,兼容性好,但存储空间浪费较多。
记录头:7~9字节
next_record
、prev_record
双向链表指针每个变长字段都存储长度信息
NULL标志:1字节表示1个字段是否为NULL,不支持bit压缩
特点:
存储密度低
页面碎片化严重
大字段处理类似COMPACT,但尾部长度列表不紧凑
SQL建表示例
CREATE TABLE t_redundant (id INT NOT NULL,name VARCHAR(100),description TEXT,created_at DATETIME,PRIMARY KEY (id)
) ROW_FORMAT=REDUNDANT;
⚡ 对比:COMPACT通过逆序长度列表和bit压缩NULL位节省了约1~2字节/字段。
3.3 溢出列处理(Off Page BLOB)
当字段过大(如VARCHAR/TEXT/BLOB超过768字节):
主数据页只存储20字节指针和部分前缀数据
超过部分存储在BLOB页(Overflow Page)
页中记录头
info_bits
标记该列为外部列
伪代码示例
if (field_length > 768) {store_prefix_in_page(field);store_remaining_in_blob_page(field);rec->info_bits |= EXTERNAL_FIELD_FLAG;
}
⚡ 类比说明:大字段就像“家具拆分存放”,主房间只留指针和小件,主房间保持整洁。
3.4 DYNAMIC与COMPRESSED行格式
3.4.1 DYNAMIC行格式
与COMPACT类似,但大字段(BLOB/TEXT/VARCHAR)默认存放在外部页
页内只存储指针,减少页内碎片化
innodb_file_per_table=ON
时效果最佳CREATE TABLE t_dynamic (id INT NOT NULL,name VARCHAR(500),content TEXT,created_at DATETIME,PRIMARY KEY(id) ) ROW_FORMAT=DYNAMIC;
优点:页利用率高,适合大字段场景
缺点:访问大字段需额外I/O
3.4.2 COMPRESSED行格式
在DYNAMIC基础上增加页压缩(zlib算法)
参数:
innodb_compression_level=6
(默认)压缩后页仍为16KB逻辑大小
适用于存储密集型表,减少磁盘占用
访问时需解压缩,增加CPU开销
3.5 COMPACT vs DYNAMIC 性能对比
指标 | COMPACT | DYNAMIC |
---|---|---|
页利用率 | 较高 | 更高,特别是大字段 |
BLOB/TEXT处理 | 部分溢出 | 全部外部存储 |
插入/更新效率 | 高 | 略低,外部页I/O增加 |
OLTP适用性 | 优秀 | 中等,适合大对象 |
⚡ 建议:OLTP场景优先使用COMPACT,大字段场景使用DYNAMIC或COMPRESSED。
3.6 源码片段解析(row0format.cc)
以COMPACT行解析为例:
/** * 解析COMPACT记录头* @rec: 页内记录指针*/
void row_parse_compact(const rec_t* rec) {const byte* field_ptr = rec->data;for (int i = 0; i < rec->n_fields; i++) {if (rec->is_varlen(i)) {int len = read_var_len(rec, i); // 从尾部长度列表读取process_var_field(field_ptr, len);field_ptr += len;} else {process_fixed_field(field_ptr, rec->field_len(i));field_ptr += rec->field_len(i);}}
}
注释:
read_var_len()
根据记录尾部长度列表倒序读取变长字段长度;固定字段按顺序存储。
💡 小结
COMPACT:紧凑、尾部长度列表、bit压缩NULL位
REDUNDANT:冗余、每字段存长度、空间浪费
DYNAMIC:大字段外部存储,页内保持紧凑
COMPRESSED:在DYNAMIC基础上增加页压缩,节省磁盘空间
行格式选择直接影响页利用率、查询性能和BLOB访问开销。理解行格式是分析InnoDB存储优化的核心。