MySQL 8.0 体系结构:一条 SQL 的完整旅程

这不是一张"架构图",是一条路径。你发出去的每一条 SQL 都会走完它,只是大部分时候你看不见。

下面这个交互版可以播放:光点会沿着真实的物理路径走一遍,走过的线会留下来。

▶ 打开交互版动图

建议在电脑上打开,窗口拉宽效果最好。里面有 SELECT 和 UPDATE 两套流程,可以单步走,也可以点任意模块看详解。

先把分层认清:三个圈,两刀切

MySQL 不是一个整体,它被切成了三层。切开它的只有两刀:

第一刀:Server 层 / 存储引擎层。

Server 层负责"理解 SQL"——认证、解析、优化、执行。引擎层负责"存取数据"。两层之间只有一个接口:handler API。

这一刀解释了一个最常见的困惑:为什么解析器、优化器整天处理 SQL,却完全不碰磁盘?因为它只决定"该怎么做",不负责"做"。

第二刀:引擎层 / 文件系统。

InnoDB 在内存里改数据(Buffer Pool),落盘是另一件事。这两件事之间存在时间差,而 MySQL 的很多行为——崩溃恢复、脏页、WAL——都是在填这个时间差。

慢 SQL 卡在哪一层,决定你查什么

这是整张图最实用的部分。同样是"慢",卡的位置不同,查法完全不一样:

卡在哪 典型现象 先看什么
连接层 Too many connections、连上就超时 max_connections、SHOW PROCESSLIST 里 Sleep 的会话
解析器 ERROR 1064 语法错、1146 表不存在 这条一般不是性能问题,是笔误
优化器 明明有索引却走了全表扫 EXPLAIN 看 type / key / rows
执行器 + 引擎 行数扫太多、回表太多 rows 估算和实际差多少、是否覆盖索引
Buffer Pool 命中率低,磁盘 IO 打满 innodb_buffer_pool_size、命中率
锁 等锁、Lock wait timeout performance_schema.data_locks

顺序上,一般先看 EXPLAIN(判断 SQL 本身),再看 Buffer Pool 命中率(判断是不是内存不够),最后看锁(判断是不是被别的事务挡住)。

UPDATE 为什么要绕两圈

写操作比读多走一段,而且要写两套日志:redo log 和 binlog。

问题是这两套日志属于不同的层——redo 是 InnoDB 的,binlog 是 Server 层的。它们必须保持一致,否则:

  • 如果 redo 提交了、binlog 没写:主库有这条数据,从库没有 → 主从不一致
  • 如果 binlog 写了、redo 没提交:崩溃恢复时主库回滚,但 binlog 已经传到从库 → 从库多数据

所以 MySQL 用了两阶段提交:

  1. redo 写 prepare 标记
  2. 写 binlog
  3. redo 写 commit 标记

只有第 3 步完成,事务才算真正提交。崩溃恢复时,如果看到 redo 是 prepare 状态,就去 binlog 里找对应的记录——找到就提交,找不到就回滚。

三个最容易记错的点

1. binlog 不是 InnoDB 的日志。

它在 Server 层,跟用什么引擎无关。所以换引擎,binlog 还在,主从复制也不受影响。这也是为什么 binlog 能做 CDC 同步(Canal、Debezium)——它记的是逻辑变更,不是某个引擎的内部状态。

2. 锁是在引擎层加的。

所以 MyISAM 没有行锁,不是"没实现",是它压根不在那个位置。同样,锁加在索引上——没走索引的 UPDATE 会锁全表,就是这么来的。

3. 事务提交 ≠ 数据落盘。

提交只保证日志落盘(innodb_flush_log_at_trx_commit=1)。数据页还是脏页,等着后台 Page Cleaner 慢慢刷。所以 MySQL 突然挂掉不会丢已提交的数据,但重启要做崩溃恢复——“断电后启动特别慢"不是坏了,是在干活。


图是用来看走向的,但真正要记住的是这三条分界线:Server 层不碰磁盘、锁在引擎层、binlog 不属于 InnoDB。剩下的细节,都能从这三条推出来。