vault backup: 2026-05-21 19:31:28

This commit is contained in:
hhs
2026-05-21 19:31:28 +08:00
parent 531d4b7d8c
commit f9d21f0026
9 changed files with 1290 additions and 78 deletions
@@ -1,5 +1,5 @@
---
tags: [MySQL, InnoDB, Clustered Index, Buffer Pool, Redo Log, Undo Log, Change Buffer, MVCC]
tags: [MySQL, InnoDB, Clustered Index, Buffer Pool, Redo Log, Undo Log, Change Buffer, MVCC, Binlog, WAL]
create time: 2026-05-16 07:30
---
@@ -125,14 +125,14 @@ graph BT
F -.-> G
G -.-> H
H -.-> I
note["注意: 叶子节点之间通过双向链表串联,维持主键有序"]
note ~~~ F
style F fill:#00B6BC,color:#fff
style I fill:#00B6BC,color:#fff
```
> [!tip] 叶子节点的秘密
> 图中虚线箭头表示叶子节点之间通过 **双向链表** 串联,且按主键顺序排列。这意味着即使你不知道具体主键,也能通过顺序扫描快速获取一段范围数据——这就是 **范围查询高效** 的根本原因。
> [!TIP] 为什么一定要用自增主键?
> 如果选随机值(如 UUID)作为主键,每次插入都可能在索引树的中间位置分叉,导致:
> - **页分裂**:16KB 页面满了要拆成两个,触发大量 I/O
@@ -165,12 +165,11 @@ flowchart LR
style SI1 fill:#FF9F43,color:#000
style CI1 fill:#00D866,color:#fff
note["注意: 虚线表示二级索引查找后需通过主键回表获取完整行数据"]
note ~~~ SI1
note ~~~ CI1
```
> [!tip] 图中的虚线是什么意思?
> 虚线表示 **回表(Back to Table)**:先通过二级索引找到主键,再用主键去聚簇索引取完整行数据。这就是为什么走二级索引查 `SELECT *` 会比走聚簇索引慢——它实际上查了两棵树。
### 覆盖索引(Covering Index)
当查询需要的列全部在一个二级索引中时,**无需回表**,直接返回结果。这比普通查询快得多。
@@ -192,6 +191,14 @@ SELECT id FROM users WHERE email = 'alice@example.com';
## Buffer Pool 详解
> [!question] 为什么需要 Buffer Pool?
>
> 先看一组数字:内存随机访问延迟约 **100 纳秒**,SSD 约 **100 微秒**,机械硬盘约 **10 毫秒**。从磁盘读比从内存读慢 **100~100,000 倍**。
>
> 如果每次 SQL 都去磁盘读写,性能会非常糟糕。**Buffer Pool 的本质就是一个大缓存**:把最常访问的数据页放在内存里,只有缓存未命中时才去磁盘取。这和浏览器缓存图片、Redis 缓存热点数据是同一个思路。
>
> 可以把 Buffer Pool 想象成你桌面上的「常用文件架」——经常用的资料就放在手边,不常用的才去档案柜(磁盘)里拿。
Buffer Pool 是 InnoDB 最重要的性能组件,它缓存了磁盘上的数据页和索引页。
```mermaid
@@ -202,10 +209,10 @@ flowchart TD
Young --> Old["Old Sub-list"]
Old --> Free["Free List"]
Note1["INSERT/UPDATE\n→ 放 New 区"]
Note2["读未命中\n→ 从磁盘加载到 New 区"]
Note3["频繁访问\n→ 保持 Young 区\n(防污染旧页)"]
Note4["不再访问\n→ 淘汰到 Free 区"]
Note1["写操作 → 放 New 区"]
Note2["读缓存未命中 → 从磁盘加载到 New 区"]
Note3["频繁访问 → 保持 Young 区"]
Note4["不再访问 → 淘汰到 Free 区"]
end
FlushList["Flush List, 脏页链表"] --> Disk["刷入磁盘 ibd 文件"]
@@ -237,15 +244,25 @@ innodb_buffer_pool_load_at_startup = ON # 启动时恢复上一次状态
## Redo Log(重做日志)
Redo Log 是 InnoDB 特有的**物理日志**,用于保证事务的 Durability。
> [!question] 为什么需要 Redo Log?先看一个问题
>
> 回顾前面写入路径,事务提交时只需要把 Redo Log 刷到磁盘,**不需要立即把脏页写回数据文件(.ibd)**。这带来了一个矛盾:
>
> 如果事务已提交,但脏页还在内存里(尚未刷盘),此时服务器突然断电——数据不就丢了吗?
>
> 答案就在于 **WAL(Write-Ahead Logging,预写日志)** 原则:**在修改数据页之前,必须先把修改内容写到日志里**。日志是顺序追加写,体积小、速度快;数据页是随机写,体积大、速度慢。只要日志先落盘了,即使数据页还没写回磁盘,crash 之后也能「重放日志」恢复到崩溃前的状态。
>
> 一句话总结:**Redo Log 让 InnoDB 用顺序写换取了随机写的安全性——事务提交只 fsync 日志,脏页由后台慢慢刷盘。**
Redo Log 是 InnoDB 特有的**物理日志**,实现了 WAL 原则,保证事务的 Durability(持久性)。它的记录粒度是「在哪个页(page)的哪个偏移(offset)改了哪些字节」,而不是 SQL 语句——这和后面要讲的 Binlog 有本质区别。
```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 也异步"]
C -->|1| D["立即 fsync 刷盘"]
C -->|2| E["写入 OS Cache,每秒 fsync"]
C -->|0| F["每秒写 OS Cache + fsync,commit 异步"]
D --> G["磁盘持久化 ✅"]
E --> G
@@ -254,12 +271,23 @@ flowchart LR
style D fill:#00D866,color:#fff
```
> [!NOTE] 物理日志 vs 逻辑日志,到底有什么区别?
>
> 假设执行 `UPDATE users SET name = 'Bob' WHERE id = 1;`
>
> | 日志类型 | 记录内容 | 恢复方式 |
> |---------|---------|---------|
> | **物理日志(Redo Log)** | `表空间 N 的第 X 页,偏移 128 字节处,写入 'Bob'` | 直接把字节写回原位置,无需解析 SQL |
> | **逻辑日志(Binlog / Undo Log)** | `UPDATE users SET name='Bob' WHERE id=1` | 重新执行这条 SQL |
>
> 物理日志恢复速度极快(直接写原始字节),但只能用于同一页结构的恢复;逻辑日志跨引擎通用,但恢复时需要经过 SQL 解析层。Redo Log 选物理日志的原因很简单:**崩溃恢复要尽可能快,越快数据库不可用时间越短**。
### Redo Log 的特性
- **循环写入**:大小固定(默认各 48MB),写满后从头覆盖
- **物理日志**:记录「在哪个页的哪个偏移改了什么字节」,而非 SQL
- **WAL 原则**:修改数据页前,日志必须先落盘——这是 crash safe 的根本保证
- **循环写入**:大小固定(默认各 48MB),写满后从头覆盖,不能无限增长
- **物理日志**:记录「在哪个页的哪个偏移改了什么字节」,恢复时直接写回字节
- **Crash Safe**:崩溃恢复时重放 Redo Log 恢复到一致状态
- **两阶段提交(2PC)**:协调 Redo Log 和 Binlog 的一致性
> [!NOTE] flush_log_at_trx_commit 三种模式
>
@@ -271,9 +299,37 @@ flowchart LR
>
> 生产环境强烈推荐 `1`。设为 `2` 能在大部分场景接近相同性能,但在 OS 崩溃时会丢数据。
### 先搞清楚:Binlog 是什么?
在讲两阶段提交之前,必须先理解另一个日志:**Binlog(Binary Log,二进制日志)**。前面一直在讲 Redo Log,但 2PC 涉及两种日志的协调,不认识 Binlog 就无法理解后面的内容。
> [!NOTE] Binlog 一句话定义
> Binlog 是 **MySQL Server 层**(不是 InnoDB 引擎层)维护的一份**逻辑日志**,记录了所有修改数据的操作。它是**主从复制**和**数据恢复(PITR)** 的基石。
把 Redo Log 和 Binlog 放在一起对比,区别一目了然:
| 对比项 | Redo Log | Binlog |
|--------|----------|--------|
| **归属** | InnoDB 引擎层(独占) | MySQL Server 层(所有引擎共享) |
| **日志类型** | 物理日志(页偏移 + 字节) | 逻辑日志(SQL 或行变更) |
| **写入方式** | 固定大小,循环覆盖 | 追加写入,文件持续增长 |
| **主要用途** | 崩溃恢复(Crash Recovery) | 主从复制、时间点恢复 |
| **生命周期** | Checkpoint 后可覆盖 | 按配置保留 N 天后清理 |
> [!TIP] 一个类比帮助记忆
>
> - **Redo Log** 像是你记在便签纸上的「操作步骤」——写完账本(数据页)后便签就可以扔掉。便签是循环使用的(固定大小),只给自己看(InnoDB 专用)。
> - **Binlog** 像是公司存档的「操作记录」——永久保存、持续累积,给审计和分支机构(从库)用。
>
> 这两种日志目的完全不同,**缺一不可**:没有 Redo Log,crash 后数据丢失;没有 Binlog,无法做主从复制和精确时间点恢复。
关于 Binlog 的三种格式(Statement / Row / Mixed)、文件结构、核心参数等详细内容,参见 → [[hhs/MySQL/07-高可用与分布式/29-Binary Log]]
---
### 两阶段提交(Two-Phase Commit)
为什么 InnoDB 需要协调 Redo Log 和 Binlog?先想一个问题:
认识了两种日志之后,问题来了:它们各自独立写入,怎么保证「两边记录的一致性」?先想一个问题:
> [!QUESTION] 如果只写一种日志,会出现什么后果?
>
@@ -309,11 +365,23 @@ flowchart LR
>
> **关键结论**:两阶段提交的本质是让 Redo Log 充当 "投票机制"——只有当事务真正安全地存在于两种日志中时才算完成。这保证了主从数据的一致性。
> [!question] 思考:为什么不让 MySQL 先写 Binlog 再写 Redo Log?
>
> 从逻辑上讲,顺序调换似乎也能做"两阶段"。但有一个关键区别:
> - **Redo Log 是 InnoDB 自己的**,由存储引擎完全控制,崩溃恢复逻辑自洽
> - **Binlog 是 Server 层的**,所有引擎共用,它无法感知 InnoDB 事务状态
>
> 如果先写 Binlog 再写 Redo Log,在 Redo Log fsync 之前崩溃了——Binlog 里已经记录了这个事务(用于主从复制),但 InnoDB 自己的数据文件里并没有这个修改,**主从数据直接不一致**。
>
> 所以顺序不能反过来。2PC 的设计让 InnoDB(Redo Log)保持「最终决策者」的角色。
---
## Undo Log(回滚日志)
与 Redo Log(向前恢复)不同,Undo Log 用于将数据**回退到修改前的状态**。它支持两个关键功能:
与 Redo Log(向前恢复)不同,Undo Log 用于将数据**回退到修改前的状态**。它本质上是 InnoDB 的「后悔药」——在你修改数据之前,先把旧值记下来,万一需要回退或者有别的事务想看旧数据,都能找到。
它主要支撑两个关键功能:**事务回滚** 和 **MVCC 多版本读取**。
```mermaid
flowchart LR
@@ -350,6 +418,14 @@ flowchart LR
## Change Buffer(变更缓冲)
> [!question] 为什么二级索引的维护会成为性能瓶颈?
>
> 二级索引的叶子节点按索引列有序排列,而不是按主键排列。这意味着对二级索引的插入很可能是**随机 I/O**——新记录可能要插入到索引树的任意位置,如果目标页恰好不在 Buffer Pool 里,就必须先从磁盘读进来。
>
> 想象你有一张千万级的用户表,每次 INSERT 都要维护 3 个二级索引,如果每次都触发随机磁盘读写……性能会严重下降。
>
> **Change Buffer 就是为了解决这个问题**:它把「二级索引页不在内存时的写操作」先缓存起来,等该页被其他查询加载到 Buffer Pool 时,再一并合并写入,从而把多次随机 I/O 合并成一次。
Change Buffer 缓存了对**非唯一二级索引页**的修改操作,等该页被读到时再合并写入磁盘。这对 INSERT-heavy 场景有显著加速效果。
```mermaid
@@ -376,6 +452,14 @@ flowchart TD
有了写入路径全景后,我们来看最后一个关键问题:**内存里的脏页什么时候被写回磁盘?**
> [!tip] 用一个类比理解 Checkpoint
>
> 把 Redo Log 想象成一本**固定页数的笔记本**,每次数据修改就往上记一笔。笔记本写满了怎么办?不能直接丢掉,因为里面记录的修改可能还没真正写到账本(磁盘数据文件)里。
>
> **Checkpoint 就是「定期把这些记录落实到账本上」的动作**——一旦某页的修改已经刷到磁盘了,那本笔记本上对应的记录就可以安全擦除了,腾出空间写新记录。
>
> Checkpoint 推进得越快,Redo Log 的空间就越充裕;推进得太慢,Redo Log 可能写满导致写入阻塞。这是 InnoDB 内部最重要的平衡之一。
### LRU List ↔ Flush List 的双链表协作
Buffer Pool 维护着两条核心链表,它们各自有不同的职责:
@@ -383,7 +467,7 @@ Buffer Pool 维护着两条核心链表,它们各自有不同的职责:
```mermaid
flowchart LR
subgraph "LRU List, 访问顺序"
Old["Old Sub-list (最近经常访问)"] --> New["New Sub-list (新加载的页)"]
New["New Sub-list (新加载的页)"] --> Old["Old Sub-list (热点页)"]
end
subgraph "Flush List, 脏污程度"