Skip to content

InnoDB 存储引擎特性详解

如果 MySQL 是一个王国,InnoDB 就是这个王国的「核心城池」——它掌管着数据存储、事务处理、并发控制的一切。

很多人用 MySQL 多年,却对 InnoDB 一知半解。今天,我们把 InnoDB 的「十八般武艺」全部拆解清楚。


InnoDB 是什么?

InnoDB 是 MySQL 的默认存储引擎,从 MySQL 5.5.5 版本开始成为默认选项(之前是 MyISAM)。

它的设计目标是:在保证事务安全的同时,提供高性能的并发能力


磁盘结构:数据怎么存?

表空间(Tablespace)

InnoDB 的磁盘存储结构经历了演变:

共享表空间(MySQL 5.6 之前默认)

ibdata1  -- 包含:数据、索引、undo log、系统信息

问题:所有表的数据都堆在一起,文件会越来越大,不好管理。

独立表空间(MySQL 5.6+ 默认开启)

sql
-- 查看表空间模式
SHOW VARIABLES LIKE 'innodb_file_per_table';

-- 独立表空间:每个表一个 .ibd 文件
-- user.ibd  -- 只包含 user 表的数据和索引
-- order.ibd -- 只包含 order 表的数据和索引

好处

  • 单表操作不影响其他表
  • 单表删除后空间能回收
  • 更方便备份和恢复

系统表空间

即使开启独立表空间,系统表空间(ibdata1)仍然存在,存储:

  • InnoDB 数据字典(表结构元信息)
  • 双写缓冲(Doublewrite Buffer)
  • Change Buffer
  • Undo Log

页(Page)

InnoDB 磁盘 I/O 的最小单位是,默认 16KB。

┌────────────────────────────────────┐
│           Page Header (38B)        │
├────────────────────────────────────┤
│         Page Body (16338B)          │
│                                     │
│    InnoDB 行记录存储在这里           │
│                                     │
├────────────────────────────────────┤
│        Page Trailer (8B)            │
└────────────────────────────────────┘

为什么是 16KB?

  • 太小:磁盘 I/O 频繁,效率低
  • 太大:内存浪费,读取不需要的数据
  • 16KB 是平衡点,刚好是 MySQL 默认 I/O 单位

行格式(Row Format)

InnoDB 支持多种行格式:

sql
-- 查看表的行格式
SHOW TABLE STATUS LIKE 'user';

-- 可选的行格式
ROW_FORMAT = {DYNAMIC | COMPRESSED | REDUNDANT | COMPACT}
格式特点适用场景
COMPACT默认格式,节省空间通用场景
DYNAMIC超长列存放在外部页超长文本/Blob
COMPRESSED压缩存储存储紧张
REDUNDANT老格式,兼容性用老系统

COMPACT 行格式结构

┌────────┬──────────────┬─────────────────────┬─────────────────────┐
│ 变长列 │ 记录头信息    │ 列1数据 [列2数据...] │ 主键值(MySQL 5.7+) │
│ 长度   │ (NULL位图等) │                     │                     │
└────────┴──────────────┴─────────────────────┴─────────────────────┘

内存结构:数据怎么读写?

Buffer Pool(缓冲池)

这是 InnoDB 最重要的内存区域,读操作先查缓存,缓存命中直接返回

┌──────────────────────────────────────────────────────────────┐
│                        Buffer Pool                            │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐         │
│  │  Page 1 │  │  Page 2 │  │  Page 3 │  │  Page 4 │  ...     │
│  │ (数据)  │  │ (数据)  │  │ (数据)  │  │ (数据)  │         │
│  │ 脏页    │  │ 干净页  │  │ 干净页  │  │ 脏页    │         │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘         │
│                                                               │
│  LRU List (最近最少使用)                                       │
└──────────────────────────────────────────────────────────────┘

Buffer Pool 工作原理

java
// 读操作:先查缓存,再查磁盘
public ByteBuffer readPage(int pageNo) {
    // 1. 先在 Buffer Pool 中查找
    Page page = findInPool(pageNo);
    if (page != null) {
        moveToHead(page);  // 命中,放到 LRU 头部
        return page.getData();
    }
    
    // 2. 缓存未命中,从磁盘加载
    page = loadFromDisk(pageNo);
    addToPool(page);  // 加入 Buffer Pool
    
    return page.getData();
}

Buffer Pool 大小配置

sql
-- MySQL 8.0+ 建议配置
SET GLOBAL innodb_buffer_pool_size = 134217728;  -- 128MB,根据服务器内存调整
SET GLOBAL innodb_buffer_pool_instances = 4;      -- 分成 4 个实例,减少锁竞争

经验公式:Buffer Pool = 服务器内存 * 0.7,预留空间给连接、排序缓存等。

LRU 淘汰策略

Buffer Pool 满了怎么办?LRU(Least Recently Used)算法

                        热点数据

         ┌─────────────────┴─────────────────┐
         │                                   │
      ┌──▼──┐                           ┌────▼───┐
      │ 热区 │←── 频繁访问的页            │ 冷区   │
      │ 5/8  │                           │ 3/8    │
      └──────┘                           └────────┘

InnoDB 的 LRU 是变种算法,分为热区(前 5/8)和冷区(后 3/8):

  • 新读取的页先进入冷区头部
  • 如果冷区数据在 1 秒后还被访问,才进入热区
  • 淘汰时从冷区尾部淘汰

为什么这样设计?

防止全表扫描把热点数据踢出内存。全表扫描一次读取大量页,如果直接进热区,会把真正热点的数据挤出去。

Change Buffer(写缓冲)

对于非唯一索引的插入和更新,先写入 Change Buffer,减少随机 I/O。

sql
-- 场景:大量 INSERT 操作
-- 没有 Change Buffer:每次插入都要随机读写磁盘更新索引
-- 有 Change Buffer:先缓存起来,定期合并

-- 查看 Change Buffer 使用情况
SHOW STATUS LIKE 'Innodb_dblwr%';

Redo Log Buffer(重做日志缓冲)

事务修改数据时,先写入 Redo Log Buffer,提交时刷盘。

sql
-- Redo Log 配置
SET GLOBAL innodb_log_buffer_size = 16777216;  -- 16MB,默认 16MB

-- Redo Log 刷盘策略(innodb_flush_log_at_trx_commit)
-- 0: 每秒刷盘,不保证事务提交时立即刷盘
-- 1: 每次事务提交立即刷盘(默认,最安全)
-- 2: 每次事务提交写到操作系统缓存,由操作系统决定刷盘时机

InnoDB 后台线程

InnoDB 不是单线程的,它有多个后台线程协同工作:

线程作用
Master Thread协调各线程,定期刷新数据
IO Thread处理异步 I/O 请求
Purge Thread清理已删除行的历史版本
Page Cleaner Thread刷新脏页到磁盘
Recover Thread崩溃恢复

关键线程详解

IO Thread

┌─────────────────────────────────────────────────────────────┐
│                        IO Thread                              │
│  read_io_threads: 4      -- 读 I/O 线程                      │
│  write_io_threads: 4     -- 写 I/O 线程                       │
│  log_io_threads: 1      -- 日志 I/O 线程                      │
│  ibuf_io_threads: 1     -- Change Buffer 合并线程            │
└─────────────────────────────────────────────────────────────┘

Page Cleaner Thread

负责把脏页(Buffer Pool 中被修改但未刷盘的页)刷新到磁盘。

sql
-- 脏页比例阈值,超过就触发刷新
SHOW VARIABLES LIKE 'innodb_max_dirty_pages_pct';
-- 默认 90%,可适当调低增加刷新频率

崩溃恢复机制

InnoDB 为什么能保证事务安全?靠的是崩溃恢复机制

恢复流程

MySQL 启动


检查数据文件完整性


读取 Redo Log,分析未提交事务


应用 Redo Log,恢复数据


回滚未提交事务(使用 Undo Log)


完成恢复,正常提供服务

Doublewrite Buffer(双写缓冲)

Redo Log 记录的是物理页的变更,但写 Redo Log 是「零散」的。

问题是:如果写入过程中系统崩溃,可能只写了半页数据,导致数据损坏

解决方案:双写缓冲。

写入流程:
1. 先把脏页复制到 Doublewrite Buffer(顺序写)
2. Doublewrite Buffer 刷盘
3. 再把脏页写入表空间(随机写)

崩溃恢复:
如果步骤 2-3 之间崩溃,原始表空间页损坏
但 Doublewrite Buffer 有完整副本,从那里恢复

面试高频追问

Q1:InnoDB 和 MyISAM 索引的区别?

结构层面

  • MyISAM:数据和索引分离(.MYD 存数据,.MYI 存索引),索引叶子节点存数据地址
  • InnoDB:数据和索引在一起(.ibd),聚簇索引叶子节点直接存数据

主键层面

  • MyISAM:主键索引和普通索引没有区别,都是非聚簇索引
  • InnoDB:主键是聚簇索引,其他索引是二级索引(存主键值)

Q2:Buffer Pool 越大越好吗?

不是。需要平衡考虑:

java
// Buffer Pool 太大可能的问题
// 1. 故障恢复时间变长(需要恢复更多数据)
// 2. 占用过多内存,影响其他组件
// 3. LRU 链表管理开销增大

// 建议配置
total RAM = 32GB
→ Buffer Pool = 24GB
→ 其他组件 = 8GB

Q3:Redo Log 和 Binlog 有什么区别?

对比项Redo LogBinlog
位置InnoDB 特有MySQL 服务层,所有引擎可用
内容物理页变更SQL 语句或行变更
作用崩溃恢复主从复制、数据恢复
格式循环写入追加写入
刷盘时机事务提交时事务完成时

总结

InnoDB 的核心特性:

  1. 事务支持:ACID 完整实现
  2. 行锁并发:高并发场景性能优秀
  3. 崩溃恢复:Redo Log + Doublewrite 保证数据安全
  4. 内存管理:Buffer Pool 减少磁盘 I/O
  5. MVCC:读不加锁,提升并发

理解 InnoDB,就是理解 MySQL 性能和安全性的根基。

基于 VitePress 构建