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 核心对比
| 特性 | InnoDB | MyISAM |
|---|---|---|
| 事务支持 | ✅ 支持(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 区头部 - 避免一次大查询将热点数据全部替换出去
脏页刷新时机:
- redo log 快写满时(强制刷脏)
- Buffer Pool 内存不足,需要淘汰脏页时
- MySQL 空闲时后台线程定期刷新
- 正常关闭 MySQL 时
6. InnoDB 存储结构
表空间 (Tablespace / .ibd 文件)
└── 段 (Segment)
├── 叶子节点段(存储真实数据行)
├── 非叶子节点段(存储索引非叶层)
└── 回滚段(undo log)
└── 区 (Extent) — 连续 64 个页 = 1MB
└── 页 (Page) — 16KB(I/O 最小单元)
└── 行 (Row) — 实际数据记录| 层级 | 大小 | 说明 |
|---|---|---|
| 页 (Page) | 16 KB | B+ 树节点,读写最小单位 |
| 区 (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 链表分成 Young 和 Old 两段:
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 前)
SELECT * FROM users WHERE name LIKE '张%' AND age > 25;
-- 联合索引 (name, age)
执行流程:
1. 引擎层根据 name LIKE '张%' 走索引 → 拿到所有"张姓"的主键
2. 引擎层逐行回表读完整数据 ← 大量回表!
3. Server 层用 age > 25 过滤 ← 在这里才过滤,前面回表全白做有 ICP(MySQL 5.6+ 默认开启)
执行流程:
1. 引擎层根据 name LIKE '张%' 走索引
2. **引擎层** 直接用 age > 25 过滤(索引里就有 age 字段!)
3. 只回表读真正命中的行收益:回表次数大幅减少,复杂 WHERE 条件场景性能可提升 2-10×。
怎么确认走了 ICP
EXPLAIN SELECT * FROM users WHERE name LIKE '张%' AND age > 25;
-- Extra 列出现 "Using index condition" → 走了 ICP
-- 出现 "Using where" → 仅 Server 层过滤,没用 ICPMulti-Range Read(MRR)
MRR 优化"通过二级索引大量回表"的场景:
没有 MRR:
二级索引按 name 排序 → 回表的主键是乱序的 → 大量随机 IO
有 MRR:
二级索引扫完后 → **先按主键排序** → 再回表 → 顺序 IO配置:
SET optimizer_switch='mrr=on,mrr_cost_based=off';典型场景:范围查询 + 大量回表(如 WHERE age BETWEEN 20 AND 30)。
Index Merge:多列各自有索引时
当 WHERE 涉及多个有独立索引的列时,MySQL 可以合并多个索引的结果:
-- 假设 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 log | InnoDB 引擎层 | 崩溃恢复(保证持久性 D) | 事务执行中 | 是(环形) |
| undo log | InnoDB 引擎层 | 回滚 + MVCC 读历史版本 | 事务执行中 | 否(按版本管理) |
| binlog | Server 层 | 主从复制 + 数据恢复 | 事务提交时 | 否(按文件名递增) |
两阶段提交(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 层包含连接器、分析器、优化器、执行器;存储引擎层负责实际的数据读写。
执行流程:
- 连接器验证身份、加载权限
- 分析器做词法+语法解析,确认表名列名是否存在
- 优化器选择索引、确定 JOIN 顺序,生成执行计划
- 执行器调用存储引擎接口,按执行计划逐行读取数据
- 存储引擎(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,因为写入前必须读页验证唯一性。