Skip to content

存储引擎与 Buffer Pool

MySQL 的数据最终由存储引擎管理。现在商业项目最常用的是 InnoDB,因为它支持事务、行锁、MVCC、崩溃恢复和外键等能力。

一句话理解:

Server 层负责解析、优化和执行 SQL,InnoDB 负责真正读写数据页、维护索引、处理事务和恢复。

MySQL 分层

mermaid
flowchart TD
    A["客户端"] --> B["连接层"]
    B --> C["Server 层<br/>解析器 / 优化器 / 执行器"]
    C --> D["存储引擎接口"]
    D --> E["InnoDB<br/>页 / B+Tree / 事务 / 锁"]
    E --> F["磁盘文件<br/>数据文件 / redo / undo"]

为什么要分层:

职责
Server 层SQL 解析、权限、优化器、执行器、binlog
存储引擎层数据页、索引、事务、锁、崩溃恢复

这解释了一个常见问题:binlog 是 Server 层日志,redo log 是 InnoDB 日志,两者职责不同。

InnoDB 为什么重要

InnoDB 提供:

  1. 聚簇索引。
  2. 事务 ACID。
  3. 行级锁。
  4. MVCC。
  5. redo log 崩溃恢复。
  6. Buffer Pool 缓存。

如果没有这些能力,商业系统里的支付、库存、订单状态流转很难保证正确性。

Buffer Pool 是什么

磁盘很慢,内存很快。InnoDB 不会每次查询都直接读磁盘,而是优先从 Buffer Pool 找数据页。

mermaid
flowchart TD
    A["查询一行数据"] --> B{"Buffer Pool 有该页吗"}
    B -- "有" --> C["直接从内存读页"]
    B -- "没有" --> D["从磁盘读页到 Buffer Pool"]
    D --> C
    C --> E["从页中找到记录"]

注意:Buffer Pool 缓存的是页,不是单独一行。默认页大小通常是 16KB。

页为什么重要

即使你只查一行:

sql
select *
from users
where id = 1001;

InnoDB 底层也通常会把这一行所在的数据页读入内存。后续如果查询同一页上的其他行,就可能直接命中 Buffer Pool。

这也是为什么:

  1. 行越短,一页能放越多行。
  2. 主键递增,热点页更集中。
  3. 随机 UUID 主键会让写入分散。
  4. 大字段放主表会降低缓存效率。

Buffer Pool 与脏页

更新数据时,InnoDB 不是每次都立刻把数据页刷到磁盘。

mermaid
flowchart TD
    A["执行 update"] --> B["修改 Buffer Pool 中的数据页"]
    B --> C["页变成脏页"]
    C --> D["写 redo log 保证崩溃可恢复"]
    D --> E["事务提交"]
    E --> F["后台线程择机刷脏页到磁盘"]

脏页是指:内存里的页已经被修改,但磁盘上的旧页还没同步。

为什么可以这样做:

  1. 直接刷数据页是随机 IO,成本高。
  2. redo log 是顺序写,成本低。
  3. 宕机后可以用 redo log 恢复已提交修改。

LRU 和冷热数据

Buffer Pool 空间有限,需要淘汰不常用的页。InnoDB 使用改进版 LRU 思路管理冷热页。

mermaid
flowchart TD
    A["新读入的数据页"] --> B["进入 Buffer Pool"]
    B --> C{"是否频繁访问"}
    C -- "是" --> D["进入热数据区域"]
    C -- "否" --> E["停留在冷数据区域"]
    E --> F["空间不足时优先淘汰"]

为什么要区分冷热:

如果一次全表扫描把大量冷数据都放进 Buffer Pool,可能把原本高频访问的订单、用户热点页挤出去,导致系统整体变慢。

Change Buffer

对于非唯一二级索引的修改,如果目标索引页不在 Buffer Pool,InnoDB 可以先把变更记录到 Change Buffer,等以后读到该页时再合并。

适合:

  1. 写多读少。
  2. 非唯一二级索引。
  3. 随机写入较多。

不适合:

  1. 唯一索引,因为必须检查唯一性。
  2. 写完马上读,因为很快就要合并。

商业场景:为什么大字段影响性能

用户表如果这样设计:

sql
create table users (
  id bigint primary key auto_increment,
  username varchar(50) not null,
  avatar_base64 longtext null,
  created_at datetime not null
) engine = InnoDB default charset = utf8mb4;

问题:

  1. 行变大。
  2. 一页能放的记录变少。
  3. Buffer Pool 能缓存的用户数量变少。
  4. 列表查询可能被大字段拖慢。

更合理:

sql
create table users (
  id bigint primary key auto_increment,
  username varchar(50) not null,
  avatar_url varchar(255) not null default '',
  created_at datetime not null
) engine = InnoDB default charset = utf8mb4;

图片本体放对象存储,数据库只保存 URL。

排查方向

现象可能原因排查
查询忽快忽慢Buffer Pool 命中率变化看缓存命中、磁盘 IO
写入偶尔抖动脏页刷盘压力看 redo、刷脏页、IO 利用率
大查询影响全站冷数据挤出热点页排查全表扫描、报表 SQL
内存很大但还是慢索引设计差,扫描页太多EXPLAIN、慢日志

面试标准回答

text
MySQL Server 层负责 SQL 解析、优化和执行,InnoDB 负责真正的数据页、索引、事务、锁和崩溃恢复。Buffer Pool 是 InnoDB 的核心缓存,缓存的是数据页和索引页,不是单独一行。查询时先看页是否在 Buffer Pool,命中则直接读内存,不命中才从磁盘加载。更新时先修改内存页形成脏页,并写 redo log 保证崩溃恢复,之后由后台线程刷脏页。理解 Buffer Pool 后就能解释为什么行要短、主键要递增、大字段要拆出、全表扫描会影响热点缓存。

关联学习:架构分层redo log 与 binlog存储结构