Skip to content

MySQL 架构与存储引擎

概念

本节覆盖 MySQL 的逻辑分层架构、一条 SQL 语句从发出到返回结果的完整链路、InnoDB 与 MyISAM 的核心差异,以及 InnoDB 的 Buffer Pool 和存储结构。这些内容是理解锁、事务、索引等高频考点的基础。


核心原理

1. MySQL 逻辑架构分层

┌─────────────────────────────────────────────┐
│                 客户端 (Client)               │
└───────────────────┬─────────────────────────┘
                    │ TCP / Unix Socket
┌───────────────────▼─────────────────────────┐
│              连接器 (Connector)               │
│  · 身份验证 · 权限查询 · 连接池管理            │
└───────────────────┬─────────────────────────┘

┌───────────────────▼─────────────────────────┐
│   查询缓存 (Query Cache) — MySQL 8.0 已移除   │
│  · 命中则直接返回,否则继续                    │
└───────────────────┬─────────────────────────┘

┌───────────────────▼─────────────────────────┐
│              分析器 (Parser)                  │
│  · 词法分析 · 语法分析 · 生成解析树            │
└───────────────────┬─────────────────────────┘

┌───────────────────▼─────────────────────────┐
│              优化器 (Optimizer)               │
│  · 选择索引 · 决定 JOIN 顺序 · 生成执行计划    │
└───────────────────┬─────────────────────────┘

┌───────────────────▼─────────────────────────┐
│              执行器 (Executor)                │
│  · 权限校验 · 调用存储引擎接口 · 组装结果集    │
└───────────────────┬─────────────────────────┘

┌───────────────────▼─────────────────────────┐
│           存储引擎 (Storage Engine)           │
│     InnoDB  /  MyISAM  /  Memory  / …        │
│  · 数据读写 · 索引管理 · 事务 · 锁            │
└─────────────────────────────────────────────┘

Server 层(连接器→执行器)与存储引擎层通过存储引擎 API 解耦,可插拔替换。


2. SELECT 语句完整执行流程

客户端发送 SQL


连接器:验证用户名密码,加载权限


查询缓存(8.0 已移除):
    命中 ──▶ 直接返回结果
    未命中 ──▶ 继续


分析器:词法 + 语法解析,识别表名、列名


优化器:选最优索引,生成执行计划


执行器:调用存储引擎接口逐行读取


存储引擎:从 Buffer Pool 或磁盘读取数据页


返回结果集给客户端

3. UPDATE 语句完整执行流程(含 redo log / binlog)

执行 UPDATE t SET c=c+1 WHERE id=1


执行器找到 id=1 的行(先查 Buffer Pool,未命中则从磁盘读入)


执行器调用 InnoDB 写接口,修改内存中的数据页


InnoDB 写 redo log(prepare 状态)
    │   ← WAL(Write-Ahead Logging)保证崩溃恢复

Server 层写 binlog(追加到 binlog 文件)
    │   ← 用于主从复制、数据恢复

InnoDB 将 redo log 标记为 commit 状态


事务提交,返回执行成功


(后台)脏页异步刷回磁盘

两阶段提交(prepare → binlog → commit)确保 redo log 与 binlog 的一致性,防止主从数据不一致。


4. InnoDB vs MyISAM 核心对比

特性InnoDBMyISAM
事务支持✅ 支持(ACID)❌ 不支持
锁粒度行锁(+ 表锁)表锁
崩溃恢复✅ redo log 自动恢复❌ 需手动修复
外键✅ 支持❌ 不支持
MVCC✅ 支持(快照读)❌ 不支持
索引类型聚簇索引(主键即数据)非聚簇索引(索引与数据分离)
全文索引✅(5.6+)
Count(*) 性能较慢(需扫描)很快(维护行数变量)
适用场景OLTP,读写混合,需要事务只读 / 读多写少,统计类

现代项目默认选 InnoDB。MyISAM 仅在极少数只读+高频 COUNT(*) 场景有优势,且 MySQL 8.0 系统表也已全部迁移至 InnoDB。


5. Buffer Pool 工作原理

Buffer Pool 是 InnoDB 在内存中维护的数据页缓存,默认页大小 16KB。

磁盘 I/O(慢)                内存 Buffer Pool(快)
┌──────────┐    读入数据页    ┌────────────────────────┐
│  .ibd    │ ─────────────▶  │  LRU 链表               │
│  数据文件 │                 │  ┌────────┬──────────┐  │
└──────────┘                 │  │ New区  │  Old区   │  │
                             │  │ (5/8)  │  (3/8)   │  │
                             │  └────────┴──────────┘  │
                             │  · 新页先进 Old 头部      │
                             │  · 1s 后再次访问才升入 New │
                             │  · Old 尾部页面被淘汰      │
                             └────────────────────────┘

改进 LRU(防止全表扫描污染缓存):

  • 新读入的页先插入 Old 区头部,不直接进 New 区
  • 只有在 Old 区停留超过 innodb_old_blocks_time(默认 1000ms)后再次被访问,才晋升到 New 区头部
  • 避免一次大查询将热点数据全部替换出去

脏页刷新时机:

  1. redo log 快写满时(强制刷脏)
  2. Buffer Pool 内存不足,需要淘汰脏页时
  3. MySQL 空闲时后台线程定期刷新
  4. 正常关闭 MySQL 时

6. InnoDB 存储结构

表空间 (Tablespace / .ibd 文件)
└── 段 (Segment)
    ├── 叶子节点段(存储真实数据行)
    ├── 非叶子节点段(存储索引非叶层)
    └── 回滚段(undo log)
        └── 区 (Extent) — 连续 64 个页 = 1MB
            └── 页 (Page) — 16KB(I/O 最小单元)
                └── 行 (Row) — 实际数据记录
层级大小说明
页 (Page)16 KBB+ 树节点,读写最小单位
区 (Extent)1 MB(64页)顺序分配,减少碎片
段 (Segment)若干区区分叶子节点与非叶子节点
表空间若干段默认每张表一个 .ibd 文件(innodb_file_per_table)

Buffer Pool:MySQL 性能的心脏

Buffer Pool 是 InnoDB 最重要的内存结构——所有数据页都在它里面读写。2025-2026 年面试要能讲清它的 LRU 改进、change buffer、预读机制这三个核心。

整体结构

┌────────────────────────────────────────────┐
│            Buffer Pool(默认 128MB)         │
├────────────────────────────────────────────┤
│  Free List   ← 空闲页链表                   │
│  LRU List    ← 数据页 + 索引页(按访问排序) │
│  Flush List  ← 脏页(待刷盘)                │
│  Change Buffer ← 二级索引写缓冲              │
│  Adaptive Hash Index ← 自适应哈希索引       │
└────────────────────────────────────────────┘

InnoDB 的"分代 LRU":解决全表扫描污染

💡 经典 LRU 在 MySQL 里行不通

场景:执行一次大表扫描,所有热点页都被刷出 LRU → 下次正常查询全部 cache miss → 系统抖动。

InnoDB 的解法:分代 LRU——把 LRU 链表分成 YoungOld 两段:

       LRU 链表(从新到老)
┌─────────────────┬─────────────────┐
│   Young (5/8)    │   Old (3/8)     │
│   真正的热数据    │   "试用区"       │
└─────────────────┴─────────────────┘
       ↑                  ↑
     最新被        全表扫描进来的页
     高频访问的页   先放这里

关键规则

  • 新读入的页先放 Old 头部(不直接污染 Young)
  • 只有页在 Old 区停留 > 1 秒后再次被访问,才能晋升到 Young
  • 配置参数:innodb_old_blocks_pct=37(默认 3/8)、innodb_old_blocks_time=1000ms

Change Buffer:二级索引写优化

INSERT INTO t VALUES (1, 'a');

主键页在内存 → 直接写主键 B+Tree

二级索引页不在内存?
  → 普通索引: 写入 change buffer(不读磁盘)
  → 唯一索引: 必须读磁盘验证唯一性,不能用 change buffer

等下次该页被读入内存时,合并(merge)change buffer

收益:把多次随机 IO 合并为一次顺序 IO,对"写多读少"的表(如日志表)写入性能可提升数倍。

配置

  • innodb_change_buffer_max_size=25(默认占 Buffer Pool 25%)
  • innodb_change_buffering=all(缓存所有 DML)

⚠️ 用唯一索引就用不上 change buffer

这是面试黄金考点:唯一索引必须读取数据页才能验证"是否已存在相同值",所以无法走 change buffer。如果业务上能用普通索引(无重复风险),不要无脑加 UNIQUE。

预读(Read-Ahead)

InnoDB 会预测即将要访问的页并提前加载,减少 IO 等待:

预读类型触发条件说明
线性预读顺序访问完一个 extent 的 ≥ 56 个页异步加载下一个 extent
随机预读(已默认关闭)一个 extent 内有 13 个页在 Buffer Pool加载剩余页

索引下推(ICP)与 MRR:MySQL 5.6+ 关键优化

Index Condition Pushdown(ICP)

索引下推让 WHERE 条件中"涉及索引列的部分"在存储引擎层就过滤,避免回表无效行。

没有 ICP(MySQL 5.6 前)

sql
SELECT * FROM users WHERE name LIKE '张%' AND age > 25;
-- 联合索引 (name, age)

执行流程:
1. 引擎层根据 name LIKE '张%' 走索引 → 拿到所有"张姓"的主键
2. 引擎层逐行回表读完整数据  ← 大量回表!
3. Server 层用 age > 25 过滤  ← 在这里才过滤,前面回表全白做

有 ICP(MySQL 5.6+ 默认开启)

sql
执行流程:
1. 引擎层根据 name LIKE '张%' 走索引
2. **引擎层** 直接用 age > 25 过滤(索引里就有 age 字段!)
3. 只回表读真正命中的行

收益回表次数大幅减少,复杂 WHERE 条件场景性能可提升 2-10×。

怎么确认走了 ICP

sql
EXPLAIN SELECT * FROM users WHERE name LIKE '张%' AND age > 25;
-- Extra 列出现 "Using index condition" → 走了 ICP
-- 出现 "Using where" → 仅 Server 层过滤,没用 ICP

Multi-Range Read(MRR)

MRR 优化"通过二级索引大量回表"的场景:

没有 MRR:
  二级索引按 name 排序 → 回表的主键是乱序的 → 大量随机 IO

有 MRR:
  二级索引扫完后 → **先按主键排序** → 再回表 → 顺序 IO

配置

sql
SET optimizer_switch='mrr=on,mrr_cost_based=off';

典型场景:范围查询 + 大量回表(如 WHERE age BETWEEN 20 AND 30)。

Index Merge:多列各自有索引时

当 WHERE 涉及多个有独立索引的列时,MySQL 可以合并多个索引的结果

sql
-- 假设 phone、email 各有索引(无联合索引)
SELECT * FROM users WHERE phone = '...' OR email = '...';

EXPLAIN 显示 type=index_merge
执行: 分别走两个索引 → 取结果 union/intersect → 回表

面试要点OR 条件能用 Index Merge,AND 条件优先用联合索引。如果你看到 type=index_merge,往往意味着应该建一个联合索引来替代两个单列索引。


redo log / undo log / binlog 三大日志关系

MySQL 三种日志是面试黄金高频题(详见 MySQL 日志体系),这里给出一个横向对比速查

日志所在层作用写入时机是否循环
redo logInnoDB 引擎层崩溃恢复(保证持久性 D)事务执行中是(环形)
undo logInnoDB 引擎层回滚 + MVCC 读历史版本事务执行中否(按版本管理)
binlogServer 层主从复制 + 数据恢复事务提交时否(按文件名递增)

两阶段提交(2PC)保证 redo log 与 binlog 一致

1. 写 redo log(prepare 状态)
2. 写 binlog
3. 提交 redo log(commit 状态)

崩溃恢复:
  ① redo prepare + binlog 完整 → 提交
  ② redo prepare + binlog 缺失 → 回滚
  → 永远保证 redo 和 binlog 内容一致

详见 MySQL 日志 — 两阶段提交


面试常问 & 怎么答

Q1:一条 SQL 语句在 MySQL 中是怎么执行的?

先说分层:MySQL 分为 Server 层和存储引擎层。Server 层包含连接器、分析器、优化器、执行器;存储引擎层负责实际的数据读写。

执行流程:

  1. 连接器验证身份、加载权限
  2. 分析器做词法+语法解析,确认表名列名是否存在
  3. 优化器选择索引、确定 JOIN 顺序,生成执行计划
  4. 执行器调用存储引擎接口,按执行计划逐行读取数据
  5. 存储引擎(InnoDB)先查 Buffer Pool,未命中则从磁盘读入

如果是 UPDATE,还涉及 redo log 的两阶段提交(prepare → 写 binlog → commit)来保证崩溃安全和主从一致。


Q2:InnoDB 和 MyISAM 有什么区别?怎么选?

核心差异:InnoDB 支持事务(ACID)、行锁、外键、MVCC、崩溃自动恢复;MyISAM 都不支持,但 COUNT(*) 更快,因为它维护了一个行数变量。

索引结构也不同:InnoDB 用聚簇索引,主键 B+ 树叶子节点直接存数据行;MyISAM 用非聚簇索引,叶子节点存的是数据文件的物理地址。

怎么选:现代项目几乎都选 InnoDB,因为需要事务、崩溃恢复和更细粒度的锁。只有非常老的只读报表系统或有大量 COUNT(*) 无过滤条件查询的场景才会考虑 MyISAM,MySQL 8.0 已经全面切到 InnoDB。


Q3:Buffer Pool 是什么?为什么需要它?

Buffer Pool 是 InnoDB 在内存中维护的数据页缓存,本质上是一块大内存区域,以 16KB 的页为单位管理。

为什么需要:磁盘 I/O 比内存慢几个数量级。把频繁访问的数据页缓存在内存里,读取时先查 Buffer Pool,写入时也先修改内存(WAL 机制),再异步刷盘,极大提升了读写性能。

内部用改进版 LRU 链表管理页的淘汰,分 New 区(热数据,5/8)和 Old 区(冷数据,3/8),新读入的页先进 Old 区,防止全表扫描把热点数据全部挤出缓存。


看到什么就先想到这类

触发信号联想方向
"SQL 执行慢" / "explain 怎么看"优化器选了什么索引?执行计划哪步最贵?
"崩溃后数据会丢吗"redo log 两阶段提交,Buffer Pool 脏页
"主从数据不一致"binlog 格式(statement/row/mixed),两阶段提交
"为什么用 InnoDB"事务、行锁、MVCC、崩溃恢复
"count(*) 慢"InnoDB 无行数缓存,需全表或索引扫描;考虑用 Redis 计数
"内存命中率低"Buffer Pool 大小配置(innodb_buffer_pool_size),LRU 策略
"表空间碎片"InnoDB 区(Extent)连续分配,OPTIMIZE TABLE 可整理

深度图解

redo log 与 binlog 两阶段提交

InnoDB 使用"两阶段提交"协议保证 redo log 和 binlog 的一致性,防止主从数据不一致:

为什么需要两阶段提交? 若不使用两阶段提交,binlog 和 redo log 可能不一致:主库用 redo log 崩溃恢复,从库用 binlog 重放,导致主从数据不一致。两阶段提交确保两者原子性地同步。


Buffer Pool 改进版 LRU

InnoDB 将 LRU 链表分为热区(63%)冷区(37%),防止全表扫描等一次性操作污染热数据:

为什么要分冷热区? SHOW FULL TABLES 等语句会触发大量页面读入,若直接插入 LRU 头部会将热数据全部挤出。冷区机制保证只有"真正多次访问的页"才晋升热区。


延伸思考

Q: SELECT * 和指定列查询有什么区别?

A: 性能上,SELECT * 无法利用覆盖索引(必须回表读完整行),而指定列查询可以通过覆盖索引避免回表,减少 I/O。维护上,SELECT * 会随表结构变化返回更多数据,增加网络传输开销,且可能导致应用层反序列化出错,生产环境应避免使用。

Q: MySQL 8.0 为什么删除查询缓存?

A: 查询缓存问题很多:① 只要对表有任何写操作,该表所有缓存立即全部失效,高并发写场景命中率极低;② 缓存 key 是完整 SQL 字符串(含空格大小写),稍有不同就无法命中;③ 维护缓存需要全局锁,多线程并发下反而成为瓶颈。整体收益不如维护成本,MySQL 8.0 彻底移除。

Q: innodb_flush_log_at_trx_commit 三个值的区别?

A: 控制 redo log 刷盘策略:0 = 每秒刷一次(可能丢 1 秒数据,性能最好);1 = 每次提交都刷盘(默认,数据最安全);2 = 每次提交写 OS 缓冲区,每秒刷盘(OS 崩溃才丢数据,折中方案)。

Q: change buffer 的作用是什么?

A: change buffer 是 Buffer Pool 的一部分,缓存对普通索引(非唯一索引)的写操作。当目标数据页不在内存中时,不直接写磁盘,而是记入 change buffer,等下次该页被读入内存时再合并(merge)。这样将多次随机 I/O 合并为一次,提升写性能。唯一索引不能使用 change buffer,因为写入前必须读页验证唯一性。