This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/MySQL/06-InnoDB 深度解析.md
T
2026-05-17 00:06:11 +08:00

284 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [MySQL, InnoDB, Clustered Index, Buffer Pool, Redo Log, Undo Log, Change Buffer, MVCC]
create time: 2026-05-16 07:30
---
# InnoDB 深度解析
## 概述
InnoDB 是 MySQL 默认且最广泛使用的存储引擎。理解它的内部机制是优化查询和排查性能问题的基础。本节深入讲解 InnoDB 的五大核心组件及其协作方式。
## 架构总览
```mermaid
graph TB
subgraph "Buffer Pool"
BP["页缓存: 16KB/page"] --> BP1["Data Pages"]
BP --> BP2["Index Pages"]
BP --> BP3["Insert Buffer"]
BP --> BP4["LRU List"]
BP --> BP5["Free List"]
BP --> BP6["Flush List"]
end
subgraph "Redo Log"
RL1["Log Buffer: 内存缓冲区"] --> RL2["物理日志文件: ib_logfile0/1"]
end
subgraph "Undo Log"
UL1["Rollback Segment"] --> UL2["Undo Logs"]
end
subgraph "磁盘数据文件"
DF1["表空间: ibdata1 / .ibd"]
end
subgraph "Change Buffer"
CB["二级索引变更缓存"]
end
Client["SQL 请求"] --> BP
BP --> RL1 -- write path
BP --> UL1 -- transaction isolation
BP --> DF1 -- read path
BP --> CB -- secondary index cache
RL1 --> RL2
```
## Clustered Index(聚簇索引)
InnoDB 的**数据行就存储在聚簇索引的叶子节点中**。这是 InnoDB 最关键的概念——没有聚簇索引就没有数据。
```mermaid
graph BT
A["主键: 100"] --> B["非叶子节点"]
C["主键: 50"] --> B
D["主键: 200"] --> E["非叶子节点"]
B --> F["叶子节点, row_id=1, name=Alice"]
B --> G["叶子节点, row_id=2, name=Bob"]
E --> H["叶子节点, row_id=3, name=Charlie"]
E --> I["叶子节点, row_id=4, name=David"]
F -.->"链表串联" G
G -.->"链表串联" H
H -.->"链表排序" I
style F fill:#00B6BC,color:#fff
style I fill:#00B6BC,color:#fff
```
> [!TIP] 为什么一定要用自增主键?
> 如果选随机值(如 UUID)作为主键,每次插入都可能在索引树的中间位置分叉,导致:
> - **页分裂**:16KB 页面满了要拆成两个,触发大量 I/O
> - **写放大**:相邻页变得不连续,顺序写变成随机写
> - **空间浪费**:页填充率下降(从 100% 降到 ~70%)
>
> 使用自增 INT/BIGINT 时,新记录总是在索引末尾追加——完美的顺序写模式。
## Secondary Index(二级索引)
所有非主键索引都是二级索引。关键点:**二级索引的叶子节点存储的是主键值**,而不是行数据。
```mermaid
graph LR
subgraph "聚簇索引, PK=id"
CI1["id=1 → full row data"]
CI2["id=2 → full row data"]
CI3["id=3 → full row data"]
end
subgraph "二级索引, idx_email=email"
SI1["email='a@x.com' → pk=1"]
SI2["email='b@x.com' → pk=2"]
SI3["email='c@x.com' → pk=3"]
end
SI1 -.回表.-> CI1
SI2 -.回表.-> CI2
SI3 -.回表.-> CI3
style SI1 fill:#FF9F43,color:#000
style CI1 fill:#00D866,color:#fff
```
### 覆盖索引(Covering Index)
当查询需要的列全部在一个二级索引中时,**无需回表**,直接返回结果。这比普通查询快得多。
```sql
-- ❌ 普通查询:走 idx_email,但 SELECT * 需要回表查聚簇索引
SELECT * FROM users WHERE email = 'alice@example.com';
-- ✅ 覆盖索引:email + name 都在索引里,不需要回表
ALTER TABLE users ADD INDEX idx_email_name (email, name);
SELECT name FROM users WHERE email = 'alice@example.com';
-- EXPLAIN 会显示 Extra: Using index
-- ⚠️ 常见误区:查其他不在索引中的列,仍需回表
-- idx_email_name 包含的是 (email, name),不包含 id
SELECT id FROM users WHERE email = 'alice@example.com';
-- 虽然 WHERE 条件匹配了索引,但 SELECT 的 id 不在索引中 → 仍需回表
```
## Buffer Pool 详解
Buffer Pool 是 InnoDB 最重要的性能组件,它缓存了磁盘上的数据页和索引页。
```mermaid
flowchart TD
subgraph "Buffer Pool, N × 16KB Pages"
LRU["LRU List"] --> New["New Sub-list"]
New --> Young["Young Sub-list"]
Young --> Old["Old Sub-list"]
Old --> Free["Free List"]
Note1["INSERT/UPDATE\n→ 放 New 区"]
Note2["读未命中\n→ 从磁盘加载到 New 区"]
Note3["频繁访问\n→ 保持 Young 区\n(防污染旧页)"]
Note4["不再访问\n→ 淘汰到 Free 区"]
end
FlushList["Flush List, 脏页链表"] --> Disk["刷入磁盘 ibd 文件"]
LRU --> FlushList
```
### Buffer Pool 关键参数
```ini
innodb_buffer_pool_size = 1G # 建议设为物理内存 50%~70%
innodb_buffer_pool_instances = 8 # 并发实例数(GB 级配置 > 1GB 时)
innodb_buffer_pool_dump_at_exit = ON # mysqld 关闭时保存缓冲池状态
innodb_buffer_pool_load_at_startup = ON # 启动时恢复上一次状态
```
> [!QUESTION] 如何判断 Buffer Pool 是否够大?
> 查看两个关键状态变量:
> ```sql
> SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
> ```
>
> | 变量 | 含义 |
> |------|------|
> | `Innodb_buffer_pool_read_requests` | Buffer Pool 读取请求总数 |
> | `Innodb_buffer_pool_reads` | 无法命中、必须回磁盘读取的次数 |
>
> 命中率 = `1 - reads / read_requests`,正常应在 **99%+**。持续低于 95% 说明需要增大 `innodb_buffer_pool_size`。
## Redo Log(重做日志)
Redo Log 是 InnoDB 特有的**物理日志**,用于保证事务的 Durability。
```mermaid
flowchart LR
A["事务修改数据页\nBuffer Pool 中的脏页"] --> B["同时写入 Redo Log Buffer"]
B --> C{flush_log_at_trx_commit}
C -->|1| D["立即刷盘"]
C -->|2| E["写入 OS Cache,每秒刷盘"]
C -->|0| F["每秒刷盘,commit 也异步"]
D --> G["磁盘持久化 ✅"]
E --> G
F --> G
style D fill:#00D866,color:#fff
```
### Redo Log 的特性
- **循环写入**:大小固定(默认各 48MB),写满后从头覆盖
- **物理日志**:记录「在哪个页的哪个偏移改了什么字节」,而非 SQL
- **Crash Safe**:崩溃恢复时重放 Redo Log 恢复到一致状态
- **两阶段提交(2PC)**:协调 Redo Log 和 Binlog 的一致性
> [!NOTE] flush_log_at_trx_commit 三种模式
>
> | 值 | 行为 | 安全性 | 性能 |
> |----|------|--------|------|
> | 1 | commit 时立即 fsync 磁盘 | 最高(ACID) | 最低 |
> | 2 | commit 时写 OS Cache,每秒 fsync | 偶尔丢失 1s 数据 | 较高 |
> | 0 | 每秒写 OS Cache + fsync,commit 异步 | 可能丢多条事务 | 最高 |
>
> 生产环境强烈推荐 `1`。设为 `2` 能在大部分场景接近相同性能,但在 OS 崩溃时会丢数据。
## Undo Log(回滚日志)
与 Redo Log(向前恢复)不同,Undo Log 用于将数据**回退到修改前的状态**。它支持两个关键功能:
```mermaid
flowchart LR
subgraph "事务 A: UPDATE users SET balance = balance - 100 WHERE id = 1"
A1["修改前备份 → Undo Log"] --> A2["修改 Buffer Pool"]
end
subgraph "事务 B: UPDATE users SET balance = balance + 100 WHERE id = 1"
B1["等待锁 / MVCC 隔离"] --> B2["读取历史版本"]
end
A1 -.提供回退能力.-> ROLLBACK["ROLLBACK 操作"]
B2 -.快照读隔离.-> SNAPSHOT["SNAPSHOT READ"]
style A1 fill:#FF9F43,color:#000
style ROLLBACK fill:#FF6B6B,color:#fff
style SNAPSHOT fill:#00D866,color:#fff
```
### Undo Log 的核心用途
- **事务回滚(ROLLBACK)**:执行相反操作还原原始值,如 UPDATE 的反向是 INSERT(新行)或 UPDATE(旧值)
- **MVCC 多版本并发控制**:通过 Read View + Undo Log Version Chain 实现不同事务看到不同的数据快照
- **灾难恢复**:配合 Redo Log,rollback 未提交的事务
> [!TIP] Undo Log vs Redo Log
>
> | 对比项 | Undo Log | Redo Log |
> |--------|----------|----------|
> | 方向 | 回退(undo) | 重做(redo) |
> | 逻辑/物理 | **逻辑日志**(记录SQL语义) | **物理日志**(记录页面偏移) |
> | 生命周期 | 事务提交后可立即回收(purge) | 必须保留到 checkpoint 之后 |
> | 空间 | 可动态增长(undo tablespace) | 固定大小、循环复用 |
## Change Buffer(变更缓冲)
Change Buffer 缓存了对**非唯一二级索引页**的修改操作,等该页被读到时再合并写入磁盘。这对 INSERT-heavy 场景有显著加速效果。
```mermaid
flowchart TD
A["INSERT 到二级索引\n页面不在 Buffer Pool 中"] --> B["写入 Change Buffer"]
B --> C["减少随机磁盘 I/O"]
D["后续读取该页\n页面进入 Buffer Pool"] --> E["合并 Change Buffer 中的操作"]
E --> F["一次性更新磁盘"]
C --> G["性能提升 20%~30%"]
E --> G
style B fill:#FF9F43,color:#000
style G fill:#00D866,color:#fff
```
> [!NOTE] Change Buffer 的限制
> - 只对**非唯一索引**生效(唯一索引需要在插入前检查冲突,必须实时读写磁盘)
> - 对 UPDATE/DELETE 同样有效
> - 可以通过 `innodb_change_buffer_max_size` 控制最大占比(默认 25%)
## 关联笔记
### 索引相关
- [[hhs/MySQL/16-B+Tree 索引原理]] — 聚簇索引 / 二级索引的数据结构基础
- [[hhs/MySQL/18-联合索引与最左前缀]] — 覆盖索引与最左前缀原则的关系
- [[hhs/MySQL/19-EXPLAIN 完全指南]] — 用 EXPLAIN 验证是否命中覆盖索引(Using index)
### 事务与并发控制
- [[hhs/MySQL/23-ACID 与原子性实现]] — Undo Log 保证原子性的核心机制
- [[hhs/MySQL/24-隔离级别与可见性]] — MVCC 配合二级索引的可见性判断
- [[hhs/MySQL/26-锁机制总览]] — InnoDB 行锁与 Buffer Pool 的协同
### 日志与高可用
- [[hhs/MySQL/29-Binary Log]] — Binlog 与 Redo Log 的两阶段提交(2PC)
### 整体架构
- [[hhs/MySQL/01-MySQL 架构与进程模型]] — Server 层与 InnoDB 引擎的分层协作