🗄️ MySQL 数据与索引存储格式详解

InnoDB 引擎 | 缓冲池 → 内存 → 磁盘 全链路解析

🏗️ 三层存储架构
🔵 Buffer Pool(缓冲池)
内存中的数据页缓存 | 16KB/页 | LRU淘汰
↓ 读写操作 ↓
🟣 Change Buffer(变更缓冲)
二级索引变更的内存缓冲 | 减少随机IO
↓ 刷盘操作 ↓
🔷 磁盘文件
.ibd 文件 | 表空间 | B+树存储
主键 ID
VARCHAR
INT/数字
变长字段
📋 核心概念速查
概念 大小 说明
Page(页) 16KB 磁盘读写的最小单位
Row(行) ~2KB 一行数据,最大65535字节
Extent(区) 1MB 64个连续页组成
Segment(段) 可变 索引/数据段的逻辑单位
Tablespace 可变 表空间,包含多个段
🎬 交互演示:查询一条数据发生了什么
// 点击按钮开始演示...
💾 InnoDB Page(数据页)结构

每个数据页固定 16KB,包含以下区域:

File Header
38 bytes
页号/上一页/下一页/Checksum
Page Header
56 bytes
行数/空闲空间/垃圾指针
Infimum
13 bytes
伪记录 sentinel
Records...
~8000 bytes
用户数据行
Supremum
13 bytes
伪记录 sentinel
Free Space
可变量
空闲区域
Slot Array
可变量
记录偏移数组
File Trailer
8 bytes
页尾校验
📝 行记录格式(Compact)
变长列长度1 变长列长度2 ... // NULL标志位 + 变长列长度列表 NULL标志位 bitmap // 允许NULL的列的位图 Row ID (可选) 6字节 // 如果没有主键 Transaction ID 6字节 // 事务ID Roll Pointer 7字节 // 回滚指针,指向undo日志 列1数据 列2数据 列3数据... // 实际列值

实际行数据示例

id=1 name=张三 age=28 address=北京市朝阳区...
🗂️ 磁盘文件组织结构

Tablespace(表空间)

逻辑存储容器,可以是系统表空间、独立表空间、或通用表空间

Segment(段)

B+树的每个层级创建一个段(叶节点段 + 非叶节点段)

Extent(区)

64个连续的 16KB Page = 1MB 空间,预分配减少碎片

Page(页)

最小I/O单位,16KB,包含页头、用户记录、页尾等

🔄 Buffer Pool 缓冲池

默认大小 128MB(可配置),以 16KB Page 为单位管理

[Page #1] id=5 name=王五 age=35 address=上海浦东... dirty ✓
[Page #2] id=3 name=李四 age=30 address=深圳南山区...
[Page #3] id=8 name=赵六 age=25 address=广州天河区...
🔀 LRU 淘汰算法

Buffer Pool 使用改进的 LRU(最近最少使用)算法

Young Sublist (热端)
最近访问的页面
默认 5/8 的缓冲池
midpoint
Old Sublist (冷端)
长时间未访问
默认 3/8 的缓冲池

访问规则

  • 🟢 命中热端 → 移到最前面
  • 🟡 预读页面 → 放冷端头部
  • 🔴 全表扫描 → 冷端末尾淘汰
  • 🟣 脏页 → 先刷盘再淘汰
📝 Change Buffer(变更缓冲)

二级索引的修改先缓存在内存中,减少随机IO

1

INSERT 触发索引修改

UPDATE users SET name='新名字' WHERE age=25

2

二级索引不在 Buffer Pool

age 索引页不在内存中,直接修改会很慢(随机IO)

3

写入 Change Buffer

将修改记录写入内存中的 Change Buffer

4

后台合并 (Merge)

当索引页被读入时,合并 Change Buffer 的修改

🌳 InnoDB B+树索引结构

主键索引(聚簇索引)的 B+树 结构,叶节点包含完整数据

主键: 50 主键: 20, 35 主键: 70, 90 id=10 name=张 age=20 id=20 name=李 age=30 id=50 name=王 age=40 id=70 name=刘 age=50 ... →∞
非叶节点(索引目录)
叶节点(包含完整数据)
叶子节点链表
🔑 聚簇索引 vs 二次索引
对比项 聚簇索引(主键索引) 二次索引(辅助索引)
存储内容 完整的行数据 索引列 + 主键值
叶节点 直接是数据行 指向主键的指针
查找过程 一次定位 二次查找(回表)
创建数量 每表一个 每列一个(可多个)
📊 B+树特性

📏 高度平衡

所有叶子节点深度相同,查找复杂度 O(log n)

🔗 有序链表

叶节点通过双向链表连接,支持范围查询

💾 磁盘友好

节点大小≈页大小(16KB),充分利用预读

SELECT 查询流程
1

SQL 解析

Parser 解析 SQL,Optimizer 选择执行计划

2

检查 Buffer Pool

在缓冲池中查找数据页是否已缓存

3

命中?→ 直接返回

缓存命中,直接从内存读取,O(1) 复杂度

4

未命中?→ 从磁盘加载

通过 B+树 定位数据页,触发磁盘 I/O

5

放入 Buffer Pool

磁盘读取后放入缓冲池,返回数据

INSERT 写入流程
1

事务开始

开启事务,获取事务ID

2

定位数据页

通过 B+树 找到应插入的数据页位置

3

写入 Buffer Pool

将数据写入缓冲池中的数据页(标记为 dirty)

4

记录 Undo Log

写入 Undo 日志,用于回滚

5

事务提交

写入 Redo Log(可选),释放锁

6

后台刷盘

Master Thread 定期将 dirty page 刷到磁盘

🏰 MySQL 完整数据流架构
📱 客户端 (JDBC/CLI) → SQL 请求
⚙️ Server 层
Parser Optimizer Cache (MySQL 8.0移除)
🔧 InnoDB 引擎
Buffer Pool
🔵 数据页缓存
🟢 索引页缓存
🟣 Change Buffer
🔴 自适应哈希
Log Buffer
📝 Redo Log
↩️ Undo Log
Doublewrite
🛡️ 防止 partial write
📄 120 页备份
⬇️ ⬇️ ⬇️ 磁盘 I/O ⬇️ ⬇️ ⬇️
💾 磁盘文件
ibdata1 (系统表空间) *.ibd (独立表空间) ib_logfile* (Redo Log) *.frm (表结构, MySQL 5.7)

🔵 Buffer Pool 中

  • 数据格式与磁盘一致(16KB Page)
  • 多状态标记:clean/dirty/new
  • 使用 LRU 链表管理淘汰
  • 支持多实例(innodb_buffer_pool_instances)

🟣 内存数据结构

  • Dictionary Cache:表结构缓存
  • Adaptive Hash:热点数据哈希索引
  • Lock Info:行锁信息
  • Change Buffer:二级索引变更缓存

🔷 磁盘存储格式

  • Row Format:Compact/Dynamic/Compressed
  • 变长字段:使用长度前缀
  • NULL 值:位图压缩存储
  • B+树:非叶节点存索引,叶节点存数据