InnoDB 存储引擎架构

举报
yd_94907100 发表于 2026/10/25 16:22:02 2026/10/25
【摘要】 拆解 InnoDB 的内存结构、磁盘结构与后台线程,理解一条 SQL 的落盘路径

很多"MySQL 慢"或"数据丢了"的问题,最后都能落到 InnoDB 的某个结构上:Buffer Pool 太小导致刷脏页抖动,redo log 配置不当导致提交变慢,undo log 无法回收导致表空间膨胀。这篇文章从内存、磁盘、线程三个维度拆开 InnoDB,并以 MySQL 8.0 为基准,顺带标出与 5.7 的差异。适合已经会写 SQL、想搞清楚"数据到底存在哪、什么时候落盘"的后端开发阅读。

一、InnoDB 在 MySQL 中的位置

MySQL 采用插件式存储引擎,Server 层负责连接、解析、优化、执行,InnoDB 负责数据的存储与事务。一条 SQL 到达 InnoDB 后,操作的单位不是"行",而是"页"(默认 16 KB)。理解这一点,后面所有设计都能串起来:Buffer Pool 缓存的是页,redo log 记录的是页的物理修改,锁加在索引记录上,MVCC 的版本链挂在行上。

二、内存结构

Buffer Pool

Buffer Pool 是 InnoDB 最大的内存块,由 innodb_buffer_pool_size 控制,生产环境通常给到物理内存的 50%~70%。它按页组织,内部维护三条链表:

  • free list:空闲页,分配时从这里取。
  • LRU list:已缓存的页,按冷热排序。
  • flush list:脏页,按最早修改的 LSN 排序,供 checkpoint 刷盘。

InnoDB 没有用朴素的 LRU,而是把 LRU 分成 young 区(约 5/8)和 old 区(约 3/8)。新读入的页先放在 old 区的头部(midpoint),只有在 innodb_old_blocks_time(默认 1000 ms)之后再次被访问,才会晋升到 young 区。这解决了全表扫描把热数据全部挤出的问题。

-- 查看缓冲池命中率与页分布
SHOW ENGINE INNODB STATUS\G
SELECT POOL_ID, POOL_SIZE, FREE_BUFFERS, DATABASE_PAGES,
       MODIFIED_DATABASE_PAGES, HIT_RATE
FROM information_schema.INNODB_BUFFER_POOL_STATS;

HIT_RATE 长期低于 99% 就说明 Buffer Pool 偏小,或者存在大量全表扫描。

Change Buffer

Change Buffer 缓存的是对非唯一二级索引页的写操作。之所以只针对非唯一索引,是因为唯一索引插入时必须读页做唯一性校验,缓存没有意义。当二级索引页不在 Buffer Pool 中时,INSERT/UPDATE/DELETE 先写 Change Buffer,等页被读入时再 merge,把多次随机写合并成一次。

它由 innodb_change_buffer_max_size 控制,默认占 Buffer Pool 的 25%。SSD 时代随机读代价下降,这个值可以调小;机械盘或写多读少的场景可以保留默认。

Log Buffer

Log Buffer 是 redo log 的内存缓冲区,默认 16 MB。事务提交时按 innodb_flush_log_at_trx_commit 决定何时刷盘:

| 取值 | 行为 | 风险 |

| --- | --- | --- |

| 1(默认) | 每次提交都 write + fsync | 最安全,性能最低 |

| 2 | 每次提交 write,每秒 fsync | 宕机丢 1 秒 |

| 0 | 每秒 write + fsync | 宕机丢 1 秒,进程崩溃也丢 |

自适应哈希索引

InnoDB 会监控索引页的访问模式,对热点等值查询自动建立哈希索引,把 B+ 树的 3~4 次页访问降为 1 次。由 innodb_adaptive_hash_index 控制,默认开启。高并发下它可能成为争用点,出现 btr_search_latch 等待时可以考虑关闭。

三、磁盘结构

表空间、段、区、页

InnoDB 的逻辑存储单位自上而下是:表空间 → 段 → 区 → 页 → 行。

  • 一个区(extent)固定 1 MB,在 16 KB 页下等于 64 个页,保证空间连续性。
  • 段分为叶子节点段和非叶子节点段,一个索引对应两个段。
  • 独立表空间(innodb_file_per_table=ON,8.0 默认)下每张表有独立的 .ibd 文件;系统表空间是 ibdata1。
-- 建表并造数据,观察表空间与页
CREATE DATABASE IF NOT EXISTS demo DEFAULT CHARACTER SET utf8mb4;
USE demo;

CREATE TABLE t_user (
  id          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  name        VARCHAR(64)     NOT NULL,
  age         TINYINT UNSIGNED NOT NULL DEFAULT 0,
  email       VARCHAR(128)    NOT NULL,
  created_at  DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  KEY idx_name_age (name, age)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

-- 造 10 万行数据
SET SESSION cte_max_recursion_depth = 10000000;

INSERT INTO t_user (name, age, email)
WITH RECURSIVE seq AS (
  SELECT 1 AS n
  UNION ALL
  SELECT n + 1 FROM seq WHERE n < 100000
)
SELECT CONCAT('user_', n), n % 80, CONCAT('u', n, '@example.com') FROM seq;

SELECT TABLE_NAME, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'demo' AND TABLE_NAME = 't_user';

行格式

8.0 默认行格式是 DYNAMIC(5.7.9 起也是),它把长变长字段完全溢出到溢出页,行内只留 20 字节指针。COMPRESSED 支持页压缩,REDUNDANT 是旧格式。每行还有三个隐藏列:DB_TRX_ID(6 字节,最后修改的事务 ID)、DB_ROLL_PTR(7 字节,指向 undo 版本链)、DB_ROW_ID(6 字节,仅在没有主键和唯一索引时生成)。

redo log

redo log 是 WAL(Write-Ahead Logging)的核心。修改数据页前先写 redo,保证崩溃后能重放。它循环写,由 checkpoint 位置和 write pos 之间的空间决定可用量。

  • 8.0.30 之前:innodb_log_file_size 默认 48 MB,两个文件。
  • 8.0.30 起:改为 innodb_redo_log_capacity,默认 100 MB,文件放在 #innodb_redo 目录,数量自适应。

redo log 的刷盘时机由 innodb_flush_log_at_trx_commit 控制,与 Log Buffer 一节对应。

undo log

undo log 有两个作用:事务回滚,以及 MVCC 的版本链。它存放在 undo 表空间,分 insert undo 和 update undo。update undo 在事务提交后不能立刻删,要等没有 ReadView 需要它时由 purge 线程清理。长事务会阻止 purge,导致 undo 表空间持续膨胀——这是线上最常见的空间问题之一。

-- 观察 undo 与 purge
SELECT NAME, COUNT FROM information_schema.INNODB_METRICS
WHERE NAME IN ('trx_rseg_history_len', 'purge_del_mark_records');

SHOW VARIABLES LIKE 'innodb_undo_tablespaces';  -- 8.0 默认 2,且已不可在线改小

Doublewrite Buffer

刷脏页时如果写到一半断电,页就损坏了。Doublewrite 的做法是先把页顺序写到 doublewrite 区域,再写回真实位置;崩溃恢复时用副本修复坏页。8.0.20 起 doublewrite 使用独立的 .dblwr 文件,不再占用系统表空间。

四、后台线程

  • Master Thread:负责调度,包括每秒/每十秒的刷脏页与 checkpoint。
  • IO Thread:读写线程,8.0 中读写分离,数量由 innodb_read_io_threads、innodb_write_io_threads 控制。
  • Purge Thread:清理不再需要的 undo 记录。
  • Page Cleaner Thread:独立刷脏页,避免阻塞用户线程。

五、一条 UPDATE 的完整路径

以 UPDATE t_user SET age = 30 WHERE id = 1 为例:

  1. 从 Buffer Pool 找 id=1 的页,没有则从磁盘读入。
  2. 加行锁,写 undo log(记录旧值)。
  3. 修改内存中的页,标记为脏页,同时写 redo log 到 Log Buffer。
  4. 提交时按 innodb_flush_log_at_trx_commit 刷 redo;binlog 按 sync_binlog 刷盘。
  5. 脏页由 Page Cleaner 在 checkpoint 推进时异步刷回磁盘。

可以看到,提交成功不等于数据页已落盘,靠的是 redo log 保证持久性。

六、常见坑 / 最佳实践

  1. 不要盲目调大 innodb_buffer_pool_size。超过物理内存会导致 swap,性能断崖式下跌。留出 20%~30% 给系统和其他进程,并监控 swap 使用。
  2. redo log 不要配得过小。小于 1 GB 时,高并发写入会频繁触发 checkpoint,出现"日志空间不足、等待刷脏页"的抖动。8.0.30+ 建议按峰值写入量设置 innodb_redo_log_capacity。
  3. 避免长事务。长事务持有 ReadView,阻止 purge,undo 表空间只增不减;同时持有锁,放大死锁概率。用 information_schema.INNODB_TRX 定期排查 trx_started 超过 60 秒的事务。
  4. innodb_flush_log_at_trx_commit=1 与 sync_binlog=1 是金融级场景的底线,牺牲的是 TPS 而不是正确性;如果确实要放宽,优先考虑 2 而不是 0。
  5. innodb_file_per_table 保持开启,便于单表 TRUNCATE 回收空间和迁移;关闭后所有表挤在 ibdata1,只能整库导出重建。

七、小结

  • InnoDB 以页(16 KB)为最小 I/O 单位,Buffer Pool 缓存页,LRU 用 young/old 分区抵御全表扫描。
  • redo log 保证持久性,undo log 支撑回滚与 MVCC,Change Buffer 优化非唯一二级索引写。
  • 表空间 → 段 → 区(1 MB,64 页)→ 页 → 行的层次,决定了空间分配与页分裂行为。
  • 提交成功只代表 redo 落盘,脏页由后台线程异步刷回,崩溃恢复依赖 redo + doublewrite。
  • 线上最需要盯的三个指标:Buffer Pool 命中率、redo 可用空间、长事务与 undo 长度。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。