Xiaojun 的技术笔记

MySQL 内存与磁盘数据结构全景

MySQL约 11 分钟读完
目录

基线版本: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/ 目录,支持在线动态调整。

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 的完整路径

  1. 定位数据页:Buffer Pool 命中(未命中则从磁盘读入)
  2. 加锁,写 Undo(旧值,形成版本链)
  3. 在 Buffer Pool 中修改数据页 → 页变为脏页,挂入 Flush List
  4. 生成 Redo,写入 Log Buffer
  5. 事务提交:Redo 刷盘(prepare)→ 写 Binlog → Redo commit(两阶段提交)
  6. 后台异步:Page Cleaner 线程根据 Checkpoint 推进,经 Doublewrite 将脏页刷到 .ibd
  7. 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):

  1. InnoDB Architecture
  2. The InnoDB Buffer Pool
  3. Change Buffer
  4. Adaptive Hash Index
  5. Redo Log
  6. InnoDB Multi-Versioning
  7. Internal Temporary Table Use in MySQL
  8. File Space Management
  9. InnoDB System Tablespace
  10. File-Per-Table Tablespaces
  11. General Tablespaces
  12. MySQL Data Dictionary
  13. Undo Tablespaces / Undo Logs:Undo Tablespaces、Undo Logs
  14. Temporary Tablespaces
  15. Doublewrite Buffer
  16. The Physical Structure of an InnoDB Index
  17. InnoDB Row Formats
  18. Clustered and Secondary Indexes
  19. InnoDB Recovery

延伸阅读: