基线版本:MySQL 8.0(InnoDB) | 关键词:Buffer Pool · Redo/Undo · 表空间 · 数据字典 · 页结构
说明:8.0 各小版本差异较大(如 8.0.20 的 Doublewrite、8.0.30 的 Redo),文中均已标注。文末附官方文档引用,正文以 [n] 标注出处。
一、总体架构
InnoDB 的设计围绕一个核心矛盾:磁盘慢、内存快,但内存易失。整套结构可归纳为三件事:
- 用内存结构缓存数据、加速访问;
- 用磁盘结构持久化数据;
- 用 WAL(Write-Ahead Logging,先写日志) 把随机写变成顺序写,并保证崩溃恢复 [1][5]。
图 1:InnoDB 内存与磁盘结构总览
二、内存结构
1. Buffer Pool:最核心的缓存
Buffer Pool 以**页(Page,默认 16KB)**为单位缓存表数据、索引、Undo 页、Change Buffer 页等,通常配置为物理内存的 50%~75% [2]。
三条链表:
| 链表 | 作用 |
|---|---|
| Free List | 空闲页,读入新页时从这里取 |
| LRU List | 已使用的页,按访问热度淘汰 |
| Flush List | 脏页,按最早修改 LSN(oldest_modification)排序,用于 Checkpoint 刷盘 |
改进版 LRU(Midpoint Insertion): 链表分为 young 区(前 5/8)和 old 区(后 3/8,innodb_old_blocks_pct=37)。新页先插入 old 区头部,在 old 区停留超过 innodb_old_blocks_time(默认 1000ms)后再次被访问,才晋升 young 区。这样一次全表扫描不会把热点数据冲掉 [2]。
图 2:Buffer Pool 的 LRU 中点插入策略
其他要点:
- 由多个 instance(
innodb_buffer_pool_instances)拆分以降低锁竞争,每个 instance 由若干 chunk(默认 128MB)组成,支持在线调整大小; - 通过 page hash(space_id + page_no → 页)快速定位缓存页;
- 支持线性预读、随机预读;
- 支持重启时 dump/load 热页列表(
ib_buffer_pool),避免冷启动 [2]。
2. Change Buffer:延迟合并二级索引的写
问题: 对非唯一二级索引做 INSERT/UPDATE/DELETE 时,若目标索引页不在 Buffer Pool,需要一次随机读。
做法: 不读页,直接把变更记录到 Change Buffer,待该页被读入内存时(或后台线程、关闭时)再合并(merge)[3]。
- 缓冲 insert、delete-mark、purge 三类操作;
- 占 Buffer Pool 比例由
innodb_change_buffer_max_size控制(默认 25%); - 持久化部分位于系统表空间,保证崩溃不丢;
- 唯一索引不能使用,因为需要读页做唯一性检查;
- 适合写多读少场景,读多写少时合并开销可能反而明显。
推荐阅读:09 | 普通索引和唯一索引,应该怎么选择?
3. Adaptive Hash Index(AHI)
InnoDB 监控索引页访问模式,对被频繁等值查询的页自动构建哈希索引,把 B+ 树逐层查找降为近似 O(1) [4]。
- 全自动,基于 Buffer Pool 中的页,无法手动指定;
- 仅对等值查询有效,范围查询、LIKE 无益;
- 默认开启,分区数
innodb_adaptive_hash_index_parts默认 8;高并发写场景可能出现 AHI 锁争用,可按需关闭。
4. Log Buffer:Redo 的内存缓冲
- 大小
innodb_log_buffer_size(默认 16MB); - 事务修改数据页产生的 Redo 记录先写入 Log Buffer;
- 刷盘由
innodb_flush_log_at_trx_commit决定:1(默认)每次提交 write + fsync;2 提交时 write 到 OS 缓存(Page Cache)、每秒 fsync;0 每秒 write + fsync [5]。
5. 其他 InnoDB 内存结构
- 锁系统(lock_sys):记录锁、间隙锁、意向锁等,按页组织在哈希表中;
- 事务系统(trx_sys):维护活跃事务列表,用于构造 ReadView 实现 MVCC [6];
- 数据字典缓存:表、索引等元数据。
6. Server 层内存(简述)
| 结构 | 说明 |
|---|---|
| Table Open Cache / Table Definition Cache | 缓存打开的表句柄与表定义 |
| Thread Cache | 复用连接线程 |
| Binlog Cache | 每会话,事务未提交前暂存 binlog(binlog_cache_size) |
| Sort / Join / Read Buffer | 每会话私有,用于排序、无索引 Join、顺序读,按需分配 |
| 内部临时表 | 8.0 默认 TempTable 引擎,内存超限(temptable_max_ram)后溢出到磁盘文件 [7] |
注意:Query Cache 在 8.0 中已被彻底移除。
三、磁盘结构
1. 表空间的逻辑层次
图 3:InnoDB 表空间的逻辑层次
- 每个索引有两个 Segment:叶子段与非叶子段;
- 空间以 Extent(1MB)为单位分配,保证同一索引的页尽量物理连续,利于顺序 IO;小表先使用零散页,避免浪费 [8]。
2. 各类表空间一览
| 表空间 | 文件 | 内容 |
|---|---|---|
| 系统表空间 | ibdata1 |
Change Buffer 持久化部分、(8.0.20 前)Doublewrite Buffer;8.0 起数据字典、Undo 已迁出 [9] |
| 独立表空间 | 表名.ibd |
默认模式(innodb_file_per_table=ON),一表一文件 [10] |
| 通用表空间 | 自定义 .ibd |
用户用 CREATE TABLESPACE 创建,多表共享 [11] |
| 数据字典 | mysql.ibd |
8.0 起数据字典存入 InnoDB 表,取代 .frm [12] |
| Undo 表空间 | undo_001、undo_002… |
8.0 默认 2 个,可在线增删,支持自动截断 [13] |
| 临时表空间 | ibtmp1 + #innodb_temp/*.ibt |
全局临时表空间 + 会话级临时表空间池,重启后清空 [14] |
3. 通用表空间 vs 独立表空间
二者都是 InnoDB 存放用户表数据与索引的容器,区别在于“表与文件的映射关系”以及“由谁来管理文件”。
| 对比项 | 独立表空间(File-Per-Table) | 通用表空间(General) |
|---|---|---|
| 创建方式 | 自动:建表时随之创建 | 手动:CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd',建表时 TABLESPACE ts1 指定 |
| 表与文件关系 | 1 表 : 1 文件 | 多表 : 1 文件 |
| 文件位置 | 默认在数据目录(可用 DATA DIRECTORY 指定) |
可放在数据目录之外,便于把冷热数据放到不同磁盘 |
| 空间回收 | DROP / TRUNCATE 后文件删除,空间直接还给 OS | 删表后文件不收缩,空间留给同一表空间的其他表;需表空间为空后 DROP TABLESPACE |
| 元数据内存开销 | 表多时文件句柄、表空间元数据多,内存占用大 | 多表共享一个文件,元数据更省 |
| 行格式 | 支持 REDUNDANT / COMPACT / DYNAMIC / COMPRESSED | 支持,但压缩表需要 FILE_BLOCK_SIZE 与 KEY_BLOCK_SIZE 匹配 |
| 迁移 / 备份 | 支持可传输表空间(FLUSH TABLES ... FOR EXPORT + 拷贝 .ibd),单表操作方便 |
粒度是整个表空间 |
选择建议:
- 默认独立表空间即可:管理直观,
OPTIMIZE TABLE/ 删表能回收空间,单表迁移和恢复灵活; - 表数量极多(数万级)、想降低元数据与文件句柄开销,或需要把一批表放到指定磁盘时,考虑通用表空间 [10][11]。
4. 数据字典包含哪些内容
**数据字典(Data Dictionary)**是“关于数据的数据”,即元数据。8.0 之前元数据散落在 .frm、.par、.trn、.opt 文件和 MyISAM 系统表中,DDL 不具备原子性;8.0 起统一存入 mysql.ibd 中的 InnoDB 事务表,并支持原子 DDL [12]。
主要内容:
| 类别 | 元数据 |
|---|---|
| 对象定义 | schema、表、列、索引、外键、检查约束、分区 |
| 程序对象 | 视图、存储过程/函数、触发器、事件、资源组 |
| 存储相关 | 表空间、数据文件、索引与表的统计信息 |
| 字符集 | 字符集、校验规则(collation) |
| 统计信息 | 列直方图(column_statistics)、表/索引统计 |
| 内部属性 | 字典版本号等 dd_properties |
要点:
- 字典表对用户隐藏,不能直接查询或修改,只能通过
INFORMATION_SCHEMA视图、SHOW语句间接访问; - 每张表的元数据还以 SDI(Serialized Dictionary Information) 的 JSON 形式冗余存放在表空间
.ibd中,可用ibd2sdi工具读取,便于字典损坏时恢复; - 权限表(
mysql.user等)属于系统表,不是数据字典表,但 8.0 中同样改为 InnoDB 存储。
5. Redo Log(重做日志)
- 作用: 保证持久性(D)。先记 Redo、后异步刷脏页;崩溃后按 Redo 重放 [5];
- 特点: 物理逻辑日志(记录“某页某偏移做了什么”),循环写、顺序写;
- LSN(Log Sequence Number,日志序列号): 全局单调递增的 8 字节整数,表示自实例创建以来产生的 Redo 字节总量。页头、Checkpoint、刷盘位置都用 LSN 标记,用于比较页新旧、确定恢复起点;
- 版本差异:
- 8.0.30 前:固定
ib_logfile0/1,容量 =innodb_log_file_size×innodb_log_files_in_group; - 8.0.30 起:由
innodb_redo_log_capacity统一控制,文件位于#innodb_redo/目录,支持在线动态调整。
- 8.0.30 前:固定
6. Undo Log(回滚日志)
记录数据修改前的旧值,双重作用 [6][13]:
- 回滚:事务失败或 ROLLBACK 时恢复原值;
- MVCC:通过行隐藏列
DB_ROLL_PTR串成版本链,配合 ReadView 实现一致性读。
结构:
- 每个 Undo 表空间含 128 个 Rollback Segment,每个有 1024 个 slot,一个 slot 对应一个事务的 Undo 段;
- 分为 Insert Undo(提交后即可释放)与 Update Undo(含 delete-mark,需等无 ReadView 依赖后由 Purge 线程清理);
- 长事务会阻止 Purge,导致 Undo 膨胀,是常见线上隐患。
7. Doublewrite Buffer(双写缓冲)
问题: 页 16KB,而操作系统/磁盘原子写通常仅 4KB,刷脏页时断电可能产生“页部分写(partial page write)”。Redo 只记录增量,无法基于损坏页恢复。
做法: 刷脏页前,先把页顺序写入 Doublewrite 区并 fsync,再写到真正位置。崩溃后若发现页损坏,则从 Doublewrite 拷贝完整页,再应用 Redo [15]。
- 8.0.20 之前位于系统表空间;
- 8.0.20 起独立为文件
#ib_<page_size>_<n>.dblwr,可通过innodb_doublewrite_dir指向单独磁盘。
8. Server 层日志
- Binlog:Server 层逻辑日志,用于复制与时间点恢复,与存储引擎无关;
- Relay Log:从库接收并回放的中继日志;
- 与 Redo 通过**两阶段提交(prepare → binlog → commit)**保持一致。
四、页内结构:InnoDB 数据的最小单元
1. 页(16KB)的组成
图 4:InnoDB 索引页结构(自上而下为页内地址由低到高)
页内查找流程:Page Directory 二分定位槽 → 槽内链表顺序遍历 [16]。
2. 行格式(默认 DYNAMIC)
一条记录在物理上的布局,从前到后依次为:
图 5:COMPACT / DYNAMIC 行格式的记录布局
前两项和记录头合称额外信息(Extra Bytes)。记录指针指向记录头与隐藏列的交界处:向左是 NULL 位图和变长字段长度列表(逆序存放),向右是真实数据;
- 记录头(5B):
delete_mark、min_rec_flag、n_owned、heap_no、record_type、下一条记录偏移等; - 隐藏列:
DB_ROW_ID(6B,无主键时自动生成)、DB_TRX_ID(6B,最近修改的事务 ID)、DB_ROLL_PTR(7B,指向 Undo 版本链); - 溢出页:TEXT/BLOB 等大字段在 DYNAMIC 格式下行内只保留 20 字节指针,数据整体存放在溢出页 [17]。
3. B+ 树索引
- 聚簇索引:叶子节点存完整行,表数据本身就是按主键组织的 B+ 树;
- 二级索引:叶子节点存“索引列 + 主键值”,查询非覆盖列需回表;
- 非叶子节点存“键 + 子页页号”,叶子页之间通过双向链表相连,适合范围扫描;
- 页大小 16KB、扇出大,3~4 层即可支撑千万至亿级数据,点查通常只需 3~4 次页访问,上层节点常驻 Buffer Pool [18]。
五、串联起来:一条 UPDATE 的完整路径
- 定位数据页:Buffer Pool 命中(未命中则从磁盘读入)
- 加锁,写 Undo(旧值,形成版本链)
- 在 Buffer Pool 中修改数据页 → 页变为脏页,挂入 Flush List
- 生成 Redo,写入 Log Buffer
- 事务提交:Redo 刷盘(prepare)→ 写 Binlog → Redo commit(两阶段提交)
- 后台异步:Page Cleaner 线程根据 Checkpoint 推进,经 Doublewrite 将脏页刷到
.ibd - Purge 线程:清理不再被任何 ReadView 引用的 Undo 与 delete-mark 记录
崩溃恢复: 从 Checkpoint LSN 开始重放 Redo(前滚),再用 Undo 回滚未提交事务;若页损坏,先由 Doublewrite 修复 [19]。
以上流程是聚簇索引这条路径,普通二级索引会涉及 Change Buffer。
六、速查表
| 结构 | 位置 | 核心作用 |
|---|---|---|
| Buffer Pool | 内存 | 页缓存,LRU 淘汰、Flush List 管理脏页 |
| Change Buffer | 内存 + ibdata1 |
延迟合并非唯一二级索引写入,减少随机读 |
| Adaptive Hash Index | 内存 | 热点页自动建哈希,加速等值查询 |
| Log Buffer | 内存 | Redo 缓冲,减少小 IO |
| Redo Log | 磁盘 | 顺序写,保证持久性与崩溃恢复 |
| Undo Log | 磁盘 | 事务回滚 + MVCC 版本链 |
| Doublewrite | 磁盘 | 防止页部分写导致页损坏 |
| 独立/通用表空间 | 磁盘 | 存放表数据与索引(B+ 树) |
mysql.ibd |
磁盘 | 事务性数据字典(8.0 取代 .frm) |
| 临时表空间 | 磁盘 | 临时表与排序、中间结果溢出 |
| Binlog | 磁盘 | Server 层逻辑日志,复制与 PITR |
结语
理解 InnoDB 存储抓住两条主线:
- 性能线: Buffer Pool、Change Buffer、AHI、Log Buffer——把随机 IO 变顺序 IO、把磁盘访问变内存访问;
- 可靠线: Redo、Undo、Doublewrite、Binlog——保证 ACID 与崩溃恢复。
两条线在 LSN、脏页、Checkpoint 上交汇。掌握这些结构及其参数,调优(Buffer Pool 命中率、Redo 容量、刷脏策略)与排障(长事务致 Undo 膨胀、Checkpoint 阻塞)就能有的放矢。
七、参考资料
MySQL 8.0 官方文档(Reference Manual):
- InnoDB Architecture
- The InnoDB Buffer Pool
- Change Buffer
- Adaptive Hash Index
- Redo Log
- InnoDB Multi-Versioning
- Internal Temporary Table Use in MySQL
- File Space Management
- InnoDB System Tablespace
- File-Per-Table Tablespaces
- General Tablespaces
- MySQL Data Dictionary
- Undo Tablespaces / Undo Logs:Undo Tablespaces、Undo Logs
- Temporary Tablespaces
- Doublewrite Buffer
- The Physical Structure of an InnoDB Index
- InnoDB Row Formats
- Clustered and Secondary Indexes
- InnoDB Recovery
延伸阅读:
- Jeremy Cole, The physical structure of InnoDB index pages
- 姜承尧,《MySQL 技术内幕:InnoDB 存储引擎(第 2 版)》,机械工业出版社
- MySQL 源码 storage/innobase