MySQL 8.4 实战(零):架构、SQL 执行链路与阅读路线
用一条 SELECT 和一笔订单事务串起连接、解析、优化器、执行器、InnoDB、Buffer Pool、redo、undo 与 binlog,并给出整套系列的推荐阅读路线。
学习 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 分库分表、路由与故障边界
→ 八:性能诊断与综合验收
需要运行本文示例或后续实验时,请先按准备篇完成可丢弃实例部署和环境基线采集;本篇只建立架构与执行链路,不记录具体实例输出。