MySQL 最常用的存储引擎是 InnoDB。理解 InnoDB 的底层架构——尤其是缓冲池(Buffer Pool)、页(Page)、日志体系(redo/undo) 和磁盘文件体系——是理解 MySQL 为什么"快"、为什么能"崩溃恢复"、以及如何调优的根基。本文从存储引擎视角系统讲解这些核心机制。
说明:事务隔离与 MVCC、redo/undo 的事务视角见「MVCC 与事务隔离级别」;B+ 树索引原理见「索引原理」。本文聚焦存储引擎的内存与磁盘架构。
InnoDB 架构可概括为"内存 + 磁盘 + 日志"三部分协同工作:
.ibd)。InnoDB 一切设计都以减少磁盘随机 I/O 为核心目标。
数据库的瓶颈在磁盘 I/O。InnoDB 把数据页缓存到内存(Buffer Pool),读取时优先命中内存,写入时先改内存(脏页),由后台线程异步刷盘。这样绝大部分操作都发生在内存中,性能大幅提升。
配置:innodb_buffer_pool_size(MySQL 8 通常建议设为物理内存的 60%~80%)。
Buffer Pool 按 16KB 的页(Page) 组织,存放多种页:
Buffer Pool 空间有限,需要淘汰不常用的页。InnoDB 使用 LRU(最近最少使用)算法,并针对数据库场景做了冷热分离优化:
Buffer Pool LRU 被分为两部分:
+-------------------------+--------------------------+
| young 区(热数据) | old 区(冷数据) |
| 默认占 5/8 (约63%) | 默认占 3/8 (约37%) |
+-------------------------+--------------------------+innodb_old_blocks_time(默认 1000ms)后再次被访问,才会晋升到 young 区——防止短时间内频繁访问但随后不再使用的页(如临时表)霸占热区。InnoDB 有顺序预读和随机预读:当检测到大量顺序访问相邻页时,会提前把一批页读入 Buffer Pool,减少随机 I/O 等待。
page cleaner)通过 checkpoint(检查点)机制把脏页刷盘。innodb_max_dirty_pages_pct(脏页比例阈值)、redo log 写满时强制刷盘。为什么不是每次写都直接落盘?因为直接落盘是随机 I/O,极慢。先改内存 + 记录 redo log(顺序写),再由后台批量刷盘(尽量顺序),是"用内存换随机 I/O,用顺序日志换随机落盘"的核心思想。
InnoDB 每个数据页 16KB,页内结构如下:
+---------------------------+
| File Header(38字节) | 记录页的校验和、页号、上/下一页指针(双向链表)
+---------------------------+
| Page Header(56字节) | 记录页的状态、记录数、槽位信息
+---------------------------+
| Infimum + Supremum 记录 | 页内最小/最大伪记录(排序边界)
+---------------------------+
| 用户记录(User Records) | 实际行数据(按主键有序存放)
+---------------------------+
| 自由空间(Free Space) | 剩余可分配空间
+---------------------------+
| Page Directory(页目录) | 槽位目录,加速页内二分查找
+---------------------------+
| File Trailer(8字节) | 页尾部校验和(与头部对比,判断页是否损坏)
+---------------------------+InnoDB 行格式(如 COMPACT)每条记录包含额外信息(变长字段长度列表、NULL 标志位、记录头)和真实数据,记录通过下一行记录的偏移构成单向链表。
事务视角的完整讲解见「MVCC 与事务隔离级别」篇,这里从存储引擎角度说明。
innodb_flush_log_at_trx_commit=1 时),数据页可稍后刷盘。ib_logfile,通过 checkpoint 循环覆盖。ibdata1 或 undo 表空间。1. 启动时扫描 redo log
2. 对已提交但未刷盘的事务,用 redo log 重放(redo 恢复)
3. 对未提交的事务,用 undo log 回滚(undo 回滚)
4. 完成恢复后对外开放解决的问题:InnoDB 数据页 16KB,而磁盘扇区通常 512B~4KB。刷盘时如果发生掉电/宕机,可能只写了一部分页(partial page),导致页损坏,且 redo log 无法修复(redo 记录的是对原页的修改,页损坏后无法重放)。
双写方案:在 dblwr 文件中为脏页先写一份副本,再写实际数据文件。若数据页损坏,可用双写副本恢复。
写脏页前:
1. 先顺序写入 doublewrite buffer(内存)
2. 批量刷到系统表空间的双写文件 dblwr(磁盘,顺序写)
3. 再写回实际的数据页文件
若第3步部分失败 -> 从双写文件副本恢复双写本质是用额外的顺序写换取随机写的数据完整性。SSD 时代部分场景会考虑关闭双写以提升性能,但默认开启保证安全。
解决的问题:辅助索引(二级索引)不是按主键顺序的,更新辅助索引时,若目标页不在 Buffer Pool,直接读盘再改是随机 I/O,很慢。
change buffer 策略:对二级索引的插入/删除/更新,如果目标页不在缓冲池,先记录到 change buffer(内存),等目标页被读到 Buffer Pool 时,再合并(merge)应用。这样避免了频繁的随机 I/O。
更新二级索引:
1. 目标页在 Buffer Pool? -> 直接改
2. 不在 -> 先写入 change buffer(内存/落盘)
3. 后续读到该页 -> merge 合并变更注意:
innodb_change_buffering 可配置(all/inserts/none 等)。InnoDB 会根据频繁访问的查询模式,在 B+ 树上自动为热点页建立哈希索引,加速等值查询(O(1))。它不需要人工维护,是"自适应"的,且只存于内存,重启后重建。
数据目录
├── ibdata1 # 系统表空间(数据字典、默认 undo)
├── ib_logfile0/1 # redo log 文件
├── undo_001/undo_002 # undo 表空间(MySQL 8 独立 undo)
└── 数据库名/
└── 表名.ibd # 每表的独立表空间(数据 + 索引)表空间(Tablespace)是逻辑上的存储容器,分为系统表空间、独立表空间(每表一个 .ibd,默认)、通用表空间。表空间内部按"段(Segment)→ 区(Extent,1MB=64个页)→ 页(Page,16KB)→ 行(Row)"四级组织。
| 参数 | 作用 | 建议 |
|---|---|---|
innodb_buffer_pool_size | 缓冲池大小 | 物理内存 60%~80% |
innodb_old_blocks_time | 冷区页晋升热区的驻留时间 | 默认 1000ms,防预读污染 |
innodb_flush_log_at_trx_commit | redo 刷盘策略 | 高可靠=1,高性能=0/2 |
innodb_max_dirty_pages_pct | 脏页比例阈值 | 触发刷盘 |
innodb_change_buffering | 变更缓冲范围 | 写多读少可开 |
innodb_buffer_pool_instances | 缓冲池实例数 | 提升并发减少锁竞争 |
| 特性 | 旧版(本文编写时,MySQL 5.7) | 当前(MySQL 8.0/8.4 LTS) |
|---|---|---|
| 默认字符集 | utf8(需显式配置 utf8mb4) | utf8mb4(MySQL 8.0 起默认) |
| 索引 | 普通 B+Tree | 降序索引、隐藏索引、函数索引(8.0+) |
| SQL 能力 | 常规查询 | 递归 CTE、窗口函数(8.0+) |
| 版本策略 | 5.7 | 8.0(主流)/ 8.4 LTS / 9.x(创新版) |
| Java 驱动 | mysql-connector-java 5.x/8.0 | mysql-connector-j 8.x/9.x |
本文基于 MySQL 5.7 编写,核心概念(索引、事务、锁、MVCC、InnoDB)在 8.0/8.4 中依然适用;8.0 的默认字符集、隐藏索引与 SQL 增强(CTE/窗口函数)是升级后的主要差异。