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

9.8 KiB
Raw Blame History

tags, create time
tags create time
MySQL
InnoDB
Clustered Index
Buffer Pool
Redo Log
Undo Log
Change Buffer
MVCC
2026-05-16 07:30

InnoDB 深度解析

概述

InnoDB 是 MySQL 默认且最广泛使用的存储引擎。理解它的内部机制是优化查询和排查性能问题的基础。本节深入讲解 InnoDB 的五大核心组件及其协作方式。

架构总览

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 最关键的概念——没有聚簇索引就没有数据。

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(二级索引)

所有非主键索引都是二级索引。关键点:二级索引的叶子节点存储的是主键值,而不是行数据。

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)

当查询需要的列全部在一个二级索引中时,无需回表,直接返回结果。这比普通查询快得多。

-- ❌ 普通查询:走 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 最重要的性能组件,它缓存了磁盘上的数据页和索引页。

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 关键参数

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 是否够大? 查看两个关键状态变量:

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。

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 用于将数据回退到修改前的状态。它支持两个关键功能:

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 场景有显著加速效果。

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%)

关联笔记

索引相关

事务与并发控制

日志与高可用

整体架构