--- tags: [MySQL, InnoDB, Clustered Index, Buffer Pool, Redo Log, Undo Log, Change Buffer, MVCC, Binlog, WAL] 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 ``` ## 写入路径全景 了解完顶层架构后,让我们跟一次完整的 **写入请求** 走一遍流程。这能帮你理解各个组件如何在同一笔事务中协同工作。 ```mermaid flowchart TD S["SQL: INSERT/UPDATE/DELETE"] --> C{页在 Buffer Pool?} C -->|命中| D["直接修改内存页"] C -->|未命中| E["从磁盘加载页到 Buffer Pool
Page Cache Miss"] E --> D D --> F["标记页为 Dirty"] F --> G["写入 Redo Log Buffer"] G --> H{是否提交 COMMIT?} H -->|否| P["Undo Log 记录回滚信息"] H -->|是| I["两阶段提交 2PC"] I --> J["先写 Redo Log → fsync 落盘"] J --> K["再写 Binlog → fsync 落盘"] K --> L["事务完成 ✅"] P --> M["后台线程: Page Cleaner
脏页刷入磁盘 .ibd"] L --> N["后台线程: Checkpoint
推进 Watermark 允许覆盖 Redo Log"] O["Change Buffer
二级索引变更缓存"] -.-> M style D fill:#00D866,color:#fff style G fill:#FF9F43,color:#000 style J fill:#FFD166,color:#000 style K fill:#FFD166,color:#000 style L fill:#00B6BC,color:#fff ``` ### 分步拆解 | 步骤 | 操作 | 涉及组件 | 同步/异步 | |------|------|----------|-----------| | ① | 检查 Buffer Pool 中是否有目标页 | Buffer Pool | 同步 | | ② | 若无,从磁盘加载(随机读 I/O) | Buffer Pool + 磁盘 | 同步 | | ③ | 修改内存页 | Buffer Pool | 同步 | | ④ | 记录 Redo Log(相同页面偏移的修改字节) | Redo Log | 同步 | | ⑤ | 若涉及二级索引且页面不在 Buffer Pool 中 | Change Buffer | 同步(只改内存) | | ⑥ | `COMMIT` → 写 Redo Log Buffer → flush | Redo Log | 同步 | | ⑦ | `COMMIT` → 写 Binlog → flush | Binary Log | 同步 | | ⑧ | 后台 Clean Thread 将脏页刷入 .ibd | Buffer Pool + 磁盘 | 异步 | > [!WARNING] 真正的磁盘 I/O 只有两步 > > - **步骤 ⑥**: Redo Log fsync(保证 crash safe) > - **步骤 ⑧**: 脏页刷入 .ibd(后台异步,与事务无关) > > 其余步骤全部在内存中完成。**这就是为什么 InnoDB 性能远优于 MyISAM**——MyISAM 每次写都要立即落盘,而 InnoDB 可以把大部分写操作延迟到后台执行。 > [!TIP] 顺序写的秘密 > > 注意到 **Redo Log 本身是顺序追加写**的!固定大小循环复用,永远往尾部写。这意味着即使磁盘是机械硬盘,Redo Log 的写入也保持极高的吞吐——因为没有任何随机 seek。 > > 对比之下,数据文件 `.ibd` 的写入是随机的(根据 B+Tree 结构分布),这是 InnoDB 最慢的部分,也是 Buffer Pool 和 Change Buffer 要极力减少它的根本原因。 --- ## 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] 叶子节点的秘密 > 图中虚线箭头表示叶子节点之间通过 **双向链表** 串联,且按主键顺序排列。这意味着即使你不知道具体主键,也能通过顺序扫描快速获取一段范围数据——这就是 **范围查询高效** 的根本原因。 > [!TIP] 为什么一定要用自增主键? > 如果选随机值(如 UUID)作为主键,每次插入都可能在索引树的中间位置分叉,导致: > - **页分裂**:16KB 页面满了要拆成两个,触发大量 I/O > - **写放大**:相邻页变得不连续,顺序写变成随机写 > - **空间浪费**:页填充率下降(从 100% 降到 ~70%) > > 使用自增 INT/BIGINT 时,新记录总是在索引末尾追加——完美的顺序写模式。 ## Secondary Index(二级索引) 所有非主键索引都是二级索引。关键点:**二级索引的叶子节点存储的是主键值**,而不是行数据。 ```mermaid flowchart 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 ``` > [!tip] 图中的虚线是什么意思? > 虚线表示 **回表(Back to Table)**:先通过二级索引找到主键,再用主键去聚簇索引取完整行数据。这就是为什么走二级索引查 `SELECT *` 会比走聚簇索引慢——它实际上查了两棵树。 ### 覆盖索引(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 详解 > [!question] 为什么需要 Buffer Pool? > > 先看一组数字:内存随机访问延迟约 **100 纳秒**,SSD 约 **100 微秒**,机械硬盘约 **10 毫秒**。从磁盘读比从内存读慢 **100~100,000 倍**。 > > 如果每次 SQL 都去磁盘读写,性能会非常糟糕。**Buffer Pool 的本质就是一个大缓存**:把最常访问的数据页放在内存里,只有缓存未命中时才去磁盘取。这和浏览器缓存图片、Redis 缓存热点数据是同一个思路。 > > 可以把 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["写操作 → 放 New 区"] Note2["读缓存未命中 → 从磁盘加载到 New 区"] Note3["频繁访问 → 保持 Young 区"] Note4["不再访问 → 淘汰到 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(重做日志) > [!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["立即 fsync 刷盘"] C -->|2| E["写入 OS Cache,每秒 fsync"] C -->|0| F["每秒写 OS Cache + fsync,commit 异步"] D --> G["磁盘持久化 ✅"] E --> G F --> G 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 的特性 - **WAL 原则**:修改数据页前,日志必须先落盘——这是 crash safe 的根本保证 - **循环写入**:大小固定(默认各 48MB),写满后从头覆盖,不能无限增长 - **物理日志**:记录「在哪个页的哪个偏移改了什么字节」,恢复时直接写回字节 - **Crash Safe**:崩溃恢复时重放 Redo Log 恢复到一致状态 > [!NOTE] flush_log_at_trx_commit 三种模式 > > | 值 | 行为 | 安全性 | 性能 | > |----|------|--------|------| > | 1 | commit 时立即 fsync 磁盘 | 最高(ACID) | 最低 | > | 2 | commit 时写 OS Cache,每秒 fsync | 偶尔丢失 1s 数据 | 较高 | > | 0 | 每秒写 OS Cache + fsync,commit 异步 | 可能丢多条事务 | 最高 | > > 生产环境强烈推荐 `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) 认识了两种日志之后,问题来了:它们各自独立写入,怎么保证「两边记录的一致性」?先想一个问题: > [!QUESTION] 如果只写一种日志,会出现什么后果? > > - **只写 Redo Log**:crash 恢复没问题,但无法做主从复制(Binlog 是复制的唯一数据源) > - **只写 Binlog**:crash 后 Binlog 可能包含了一个实际没有落盘的数据页的事务 → 主从不一致 > > 答案:**两种情况都会导致 crash 恢复后,Redo Log 和 Binlog 之间的数据不一致。** ```mermaid flowchart LR subgraph "正常事务流程" A1["步骤1: 修改 Buffer Pool
标记页为 Dirty"] --> A2["步骤2: 写 redo log prepare"] A2 --> A3["步骤3: fsync redo log"] A3 --> A4["步骤4: 写 binlog"] A4 --> A5["步骤5: fsync binlog"] A5 --> A6["步骤6: 写 redo log commit"] A6 --> A7["步骤7: fsync redo log"] A7 --> T1["事务成功 ✅"] end style A3 fill:#FFD166,color:#000 style A5 fill:#FF9F43,color:#000 style A7 fill:#FFD166,color:#000 ``` > [!TIP] 核心思想:prepare = "我准备好了但不确定",commit = "确认执行" > > | Crash 时间点 | Redo Log 状态 | Binlog 状态 | 恢复行为 | > |---|---|---|---| > | A2 ~ A3 之间 | prepare(未刷盘) | 未写 | Redo Log 回滚 → **Binlog 也不会有这条记录** ✅ | > | A3 ~ A4 之间 | prepare(已刷盘) | 未写 | Redo Log 回滚到 prepare 之前 → **事务被丢弃** ✅ | > | A5 之后 | commit(已刷盘) | 已写 | Redo Log 完整重放 → **事务完整恢复** ✅ | > > **关键结论**:两阶段提交的本质是让 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 用于将数据**回退到修改前的状态**。它本质上是 InnoDB 的「后悔药」——在你修改数据之前,先把旧值记下来,万一需要回退或者有别的事务想看旧数据,都能找到。 它主要支撑两个关键功能:**事务回滚** 和 **MVCC 多版本读取**。 ```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(变更缓冲) > [!question] 为什么二级索引的维护会成为性能瓶颈? > > 二级索引的叶子节点按索引列有序排列,而不是按主键排列。这意味着对二级索引的插入很可能是**随机 I/O**——新记录可能要插入到索引树的任意位置,如果目标页恰好不在 Buffer Pool 里,就必须先从磁盘读进来。 > > 想象你有一张千万级的用户表,每次 INSERT 都要维护 3 个二级索引,如果每次都触发随机磁盘读写……性能会严重下降。 > > **Change Buffer 就是为了解决这个问题**:它把「二级索引页不在内存时的写操作」先缓存起来,等该页被其他查询加载到 Buffer Pool 时,再一并合并写入,从而把多次随机 I/O 合并成一次。 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%) ## Checkpoint & Page Clean / Flush 有了写入路径全景后,我们来看最后一个关键问题:**内存里的脏页什么时候被写回磁盘?** > [!tip] 用一个类比理解 Checkpoint > > 把 Redo Log 想象成一本**固定页数的笔记本**,每次数据修改就往上记一笔。笔记本写满了怎么办?不能直接丢掉,因为里面记录的修改可能还没真正写到账本(磁盘数据文件)里。 > > **Checkpoint 就是「定期把这些记录落实到账本上」的动作**——一旦某页的修改已经刷到磁盘了,那本笔记本上对应的记录就可以安全擦除了,腾出空间写新记录。 > > Checkpoint 推进得越快,Redo Log 的空间就越充裕;推进得太慢,Redo Log 可能写满导致写入阻塞。这是 InnoDB 内部最重要的平衡之一。 ### LRU List ↔ Flush List 的双链表协作 Buffer Pool 维护着两条核心链表,它们各自有不同的职责: ```mermaid flowchart LR subgraph "LRU List, 访问顺序" New["New Sub-list (新加载的页)"] --> Old["Old Sub-list (热点页)"] end subgraph "Flush List, 脏污程度" FL1["Clean Pages (已刷盘)"] --> FL2["Dirty Pages (未刷盘)"] end DirtyInLRU["LRU 中的脏页
同时存在于 Flush List"] --> FL2 CleanedInLRU["已被刷盘的页
仍在 LRU 中等待淘汰"] --> FL1 style DirtyInLRU fill:#FF6B6B,color:#fff style CleanedInLRU fill:#00D866,color:#fff ``` | 链表 | 排序依据 | 主要用途 | |------|----------|----------| | **LRU List** | 访问时间(从旧到新) | 决定哪些页该被淘汰出 Buffer Pool | | **Flush List** | 是否脏污 + LSN 递增 | 决定哪些页需要刷入磁盘 | > [!QUESTION] 为什么需要两条独立的链表? > > 想象一个场景:Buffer Pool 满了,需要回收一页空间。 > - **只查 LRU**:可能取到 dirty page → 必须先刷盘再回收 → 慢 > - **只查 Flush**:可能取到 clean page 但很久没访问过 → 浪费了干净页的空间 > > 实际实现中,InnoDB 会从 LRU 的 Old 端找一个 dirty page 刷盘;如果一时找不到,也会刷干净页来救急。**这两条链表的配合是 Buffer Pool 高效运转的关键。** ### Checkpoint: Redo Log 循环复用的水坝 既然 Redo Log 是固定大小、循环重写的,那么问题来了:**旧日志什么时候能被安全覆盖?** ```mermaid flowchart LR A["Redo Log 写满尾部"] --> B{"能从头覆盖吗?"} B -->|"否"| C["Page Cleaner: 刷脏页到磁盘"] C --> D["Flush List 中的脏页减少"] D --> E["Checkpoint Thread: 推进 Checkpoint LSN"] E --> F["Watermark 前进 → 旧日志可覆盖 ✅"] F --> G["头部开始循环写入"] B -->|"是"| G style F fill:#00D866,color:#fff style G fill:#FFD166,color:#000 ``` | 概念 | 含义 | 关键参数 | |------|------|----------| | **LSN (Log Sequence Number)** | 单调递增的日志序号,每个页也有当前 LSN | — | | **Checkpoint LSN** | 到此位置之前的所有页保证已落盘 | — | | **Redo Log 可覆盖分界** | `Checkpoint LSN < X < Current LSN` 之间的日志不能删 | — | | **innodb_max_dirty_pages_pct** | 脏页占 Buffer Pool 的上限百分比 | 默认 75% | > [!WARNING] 脏页过多的危险信号 > > 当脏页比例超过 `innodb_max_dirty_pages_pct` 时,InnoDB 会触发 **流量控制(Throttling)**: > 1. 停止接受新的写请求 > 2. 优先执行 Page Clean → 刷脏页 > 3. 等比例回落后才恢复写操作 > > 这会导致 **突发性写入延迟飙升**。监控 `Innodb_dblwr_pages_written` 和 `Innodb_buffer_pool_dirty_pages` 可以提前发现这个问题。 > > **常见原因**: Buffer Pool 太小、大量批量更新、事务提交过于密集导致 redo flush 跟不上。 --- ## 关联笔记 ### 索引相关 - [[hhs/MySQL/03-索引与查询优化/12-B+Tree 索引原理]] — B+Tree 数据结构基础 - [[hhs/MySQL/03-索引与查询优化/13-聚簇索引与二级索引]] — 聚簇索引与二级索引的深度对比 - [[hhs/MySQL/03-索引与查询优化/14-联合索引与最左前缀]] — 覆盖索引与最左前缀原则的关系 - [[hhs/MySQL/03-索引与查询优化/15-EXPLAIN 完全指南]] — 用 EXPLAIN 验证是否命中覆盖索引(Using index) ### 事务与并发控制 - [[hhs/MySQL/06-事务与并发控制/23-ACID 与原子性实现]] — Undo Log 保证原子性的核心机制 - [[hhs/MySQL/06-事务与并发控制/24-隔离级别与可见性]] — 四种隔离级别的实际区别 - [[hhs/MySQL/06-事务与并发控制/25-MVCC 原理]] — Read View + Undo Version Chain 的完整实现 - [[hhs/MySQL/06-事务与并发控制/26-锁机制总览]] — InnoDB 行锁与 Buffer Pool 的协同 - [[hhs/MySQL/06-事务与并发控制/28-一致性读与当前读]] — 二次读与快照读的触发条件 ### 日志与恢复 - [[hhs/MySQL/07-高可用与分布式/29-Binary Log]] — Binlog 与 Redo Log 的两阶段提交(2PC) - [[hhs/MySQL/07-高可用与分布式/34-备份与恢复]] — 全量备份 + Redo Log 恢复策略 ### 性能调优 - [[hhs/MySQL/08-工程实践/38-监控指标]] — 生产环境关键监控项(Buffer Pool命中率、脏页比例等) - [[hhs/MySQL/08-工程实践/40-常见踩坑]] — InnoDB 常见性能陷阱及排查方法 ### 整体架构 - [[hhs/MySQL/01-入门基础/01-MySQL 架构与入门]] — Server 层与 InnoDB 引擎的分层协作 - [[hhs/MySQL/04-存储引擎/20-其他存储引擎概览]] — MyISAM / Memory / Archive 的特点对比