学习 MySQL 时最容易遇到两种割裂:一种只会写 SQL,却不知道执行计划、锁和日志发生在哪里;另一种背了 Buffer Pool、redo、binlog 等名词,却无法把它们连接到一次请求。本文不追求讲完每个内部细节,而是先建立一张稳定地图,后面的专题再逐层放大。

一、本文讲什么、解决什么问题

本文将帮助你建立 MySQL 运行机制的整体认识,并为后续专题提供一张阅读与排障地图。通过本文,你可以:

  • 梳理一条 SQL 从客户端进入 MySQL 到访问数据的主要环节;
  • 区分查询执行与事务提交两条核心链路;
  • 理解 Buffer Pool、undo、redo 和 binlog 在整体架构中的位置与职责;
  • 将查询慢、锁等待、提交延迟等现象初步映射到相应环节;
  • 根据具体问题找到准备篇与 M01~M13 中适合继续阅读的专题。

本文默认读者已经能够编写基本的 SELECT、INSERT、UPDATE、DELETE 和 JOIN。如果尚不熟悉 NULL 三值逻辑、聚合、连接语义和事务基本语法,建议先补充 SQL 基础。

二、先看一条 SQL 的逻辑链路

一条 SQL 从客户端到数据页,可以先简化为:

客户端/连接池
  → MySQL 连接与会话
  → SQL 解析、名称解析与权限检查
  → 优化器选择执行计划
  → 执行器驱动计划中的迭代器
  → 存储引擎接口
  → InnoDB 索引、Buffer Pool、锁与 MVCC
  → 返回结果或进入事务提交

这条链路不是严格的源码调用栈,而是排障时使用的职责模型。可以先按以下方式理解各组件的归属和职责:

组件 所属层次 主要职责
连接管理 MySQL Server 建立连接、完成身份认证、维护会话状态,接收 SQL 并返回结果
SQL 解析与语义处理 MySQL Server 解析 SQL,完成对象和列的名称解析以及与语句相关的权限检查
优化器 MySQL Server 在语义允许的范围内,根据统计信息和成本估算选择表访问顺序、访问路径与连接策略
执行器 MySQL Server 按执行计划驱动各个迭代器,处理过滤、连接、排序和聚合,并通过存储引擎接口读写数据
InnoDB 存储引擎 管理表和索引、数据页、Buffer Pool、MVCC、行级锁、undo、redo 与崩溃恢复

优化器不只决定 JOIN 的表访问顺序,还会比较全表扫描、主键访问、二级索引和 Index Merge 等候选访问路径。SQL 中的表书写顺序不一定就是实际访问顺序,最终选择可以通过 EXPLAIN 中的表顺序、type、possible_keys 和 key 等字段观察。

作出这些选择时,InnoDB 会提供索引基数、范围行数等统计和估算信息;Server 层还可以使用列直方图与成本模型。优化器综合这些信息形成执行计划,存储引擎不会在执行阶段自行改选另一个索引。

MySQL Server 与 InnoDB 通过存储引擎接口协作。执行器负责驱动优化器生成的执行计划,InnoDB 则按照计划完成具体的索引或表访问,并处理记录版本可见性和行级并发控制。以下边界在阅读后续章节和排障时尤其重要:

  • binlog 属于 MySQL Server,redo 和 undo 属于 InnoDB;
  • 元数据锁 MDL 由 Server 层管理,记录锁、间隙锁等由 InnoDB 管理;
  • 启用 binlog 时,事务提交需要由 Server 层与存储引擎共同协调;
  • 执行计划由 Server 层驱动,但实际的数据页读写、Buffer Pool 缓存和崩溃恢复由 InnoDB 完成。

2.1 连接不是一个无状态管道

客户端连入 MySQL 后得到一个会话。会话会携带影响行为的状态,例如:

  • 当前数据库和账号权限;
  • autocommit 与当前事务;
  • transaction_isolation;
  • time_zone、字符集与排序规则;
  • SQL mode 和部分 optimizer switch。

同一条 SQL 在两个会话中结果不同,不一定是数据变化,也可能是会话状态不同。保存实验或故障证据时,不能只有 SQL 文本,还要记录连接 ID、事务边界和关键会话变量。

2.2 解析正确不等于执行高效

Server 先判断 SQL 是否能被正确解析、对象和列是否存在、当前账号是否有权限。之后优化器根据索引、统计信息、谓词和成本模型选择计划。执行器再按计划驱动表访问、连接、过滤、排序与聚合。

因此要区分三类问题:

问题 示例 主要证据
语义/权限 表不存在、列歧义、权限不足 错误码和 SQL
计划选择 估算错误、连接顺序不合理 EXPLAIN、统计信息
实际执行 锁等待、扫描过多、I/O 或 CPU 压力 EXPLAIN ANALYZE、Performance Schema、状态指标

三、SELECT 如何走到一行数据

先给出理解读路径所需的最小表结构。这里省略了与本节无关的业务字段、约束和索引,完整设计见实战(一):InnoDB 与订单表结构设计。

CREATE TABLE orders (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  order_no VARCHAR(32) NOT NULL,
  user_id BIGINT UNSIGNED NOT NULL,
  total_amount DECIMAL(12,2) NOT NULL,
  created_at DATETIME(6) NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY uk_orders_order_no (order_no),
  KEY idx_orders_user_created (user_id,created_at,id)
) ENGINE=InnoDB;

PRIMARY KEY (id) 是聚簇索引;idx_orders_user_created 是服务于用户订单列表的二级索引。以下查询用于观察从二级索引定位候选记录、再访问聚簇索引完整行的读路径:

SELECT id, order_no, total_amount, created_at
FROM orders
WHERE user_id = 4242
ORDER BY created_at DESC, id DESC
LIMIT 20;

优化器首先判断是否存在能够定位 user_id 并提供排序的访问路径。如果选择 idx_orders_user_created,InnoDB 会在该 B+Tree 中定位一个用户的连续范围,并反向读取最近的索引记录。二级索引叶子包含二级键和主键值;order_no 和 total_amount 不在这个索引中,因此还要根据主键访问聚簇索引记录,也就是通常所说的回表。

二级索引定位 user_id 范围
  → 按 created_at、id 逆序读取候选主键
  → 访问聚簇索引读取 order_no、total_amount
  → 根据 read view 判断版本可见性
  → 返回最多 20 行

如果目标页已经在 Buffer Pool,读取可以主要发生在内存;不在时需要从表空间读取页。所谓“Buffer Pool 命中率高”只说明较少发生物理页读取,不代表查询扫描行少、没有锁等待或 CPU 成本低。

普通 SELECT 在常见的 RC/RR 隔离级别下通常是一致性非锁定读。InnoDB 根据 read view 判断记录版本是否可见,不可见时可能沿 undo 版本链还原旧版本。SELECT ... FOR UPDATE 则是锁定读,需要读取并锁住当前可操作版本。二者不能用同一套“快照”口诀解释。

四、一笔写事务如何提交

以支付回调推进订单为例。沿用第一篇的状态码:支付 10=pending、20=succeeded,订单 10=new、20=paid:

START TRANSACTION;
UPDATE payments
SET state = 20, paid_at = CURRENT_TIMESTAMP(6)
WHERE payment_no = ? AND state = 10;

UPDATE orders
SET state = 20, updated_at = CURRENT_TIMESTAMP(6)
WHERE id = ? AND state = 10;
COMMIT;

执行 DML 时,InnoDB 定位并锁定当前记录,在 Buffer Pool 页上修改数据,生成回滚和一致性读需要的 undo 信息,并产生崩溃恢复需要的 redo。启用 binary log 时,Server 还要生成 binlog 事务。提交路径必须协调 InnoDB redo 和 Server binlog。

BEGIN
  → 定位并锁定记录
  → 生成 undo/redo,修改 Buffer Pool 页
  → Server 形成 binlog 事务
COMMIT
  → InnoDB prepare
  → 按配置写入/同步 binlog
  → InnoDB commit
  → 返回客户端
后台
  → 刷脏页并推进 checkpoint

这张图需要保留几个边界:

  • undo、redo 和 binlog 不是三份相同日志;
  • COMMIT 不要求所有数据页同步写回表空间;
  • 组提交会让多个事务共享日志刷盘工作;
  • 客户端超时不等于事务回滚,可能出现提交结果未知;
  • DDL 具有自己的事务与元数据锁边界,不能按普通 DML 回滚模型理解。

五、从逻辑表到磁盘对象

常见对象可以分成四层:

层次 对象 主要问题
业务逻辑 用户、订单、支付、唯一业务号 哪些事实必须始终成立
SQL/schema 表、列、主键、索引、约束、分区 如何表达并高效访问事实
InnoDB 表空间、段、区、页、记录、Buffer Pool 数据如何组织、缓存和修改
日志与恢复 undo、redo、binlog、relay log、备份 如何回滚、恢复、复制和审计

一张 InnoDB 表通常以聚簇索引组织行,二级索引提供其他访问顺序。页是 Buffer Pool 和表空间 I/O 的基本单位之一,不是“一行对应一个磁盘位置”。表空间文件大小、逻辑行数和索引大小也不会严格同步变化:删除记录后页可以先在表空间内部复用,而不是立即归还给操作系统。

原生分区仍在一个 MySQL 实例和逻辑表内;业务分片则把数据路由到多个表或实例。二者位于 schema/部署层,不会改变“每个 InnoDB 分片内部仍使用页、索引和日志”的事实。

六、延迟应该映射到链路

“数据库慢”至少可能对应以下位置:

连接获取或认证
→ 元数据锁等待
→ 行锁等待
→ 优化器选择错误计划
→ 扫描、回表、连接、排序或聚合工作过多
→ Buffer Pool miss 与存储 I/O
→ redo/binlog 刷盘
→ 副本应用延迟
→ 客户端网络和调度

诊断时先确定时间窗口和具体 SQL,再选择证据。慢日志能发现候选,执行计划说明访问路径,Performance Schema 能关联语句与等待,InnoDB 状态和全局计数器提供实例压力背景。任何一个指标都不应该单独替代整条证据链。

七、推荐阅读路线

零:架构与 SQL 执行链路
→ 准备篇:实验环境部署与基线
→ 一:表结构与数据类型
→ 二:主键与索引
→ 三:优化器与执行计划
→ 五:事务、MVCC、锁与死锁
→ 六:redo、undo、checkpoint 与崩溃恢复
→ 七:binlog、GTID 与复制
→ 九:备份、恢复与 PITR
→ 十二:一主多从、双 M 误区与故障切换
→ 十三:InnoDB Cluster、Router 与高可用故障恢复
→ 十:在线 DDL、MDL 与结构变更
→ 四:分区、分表与扩容
→ 十一:ShardingSphere-Proxy 分库分表、路由与故障边界
→ 八:性能诊断与综合验收

需要运行本文示例或后续实验时,请先按准备篇完成可丢弃实例部署和环境基线采集;本篇只建立架构与执行链路,不记录具体实例输出。

参考资料