MySQL 其实是 上层组件、下层存储引擎 组合起来的,结构可以理解为:
┌─────────────────────────────────┐
│ Server 层 │
│ (连接器、查询缓存、分析器、 │
│ 优化器、执行器、内置函数等) │
└────────────┬────────────────────┘
│ 统一的 API 接口
┌────────────▼────────────────────┐
│ 存储引擎层 │
│ (InnoDB、MyISAM、Memory 等) │
│ 负责数据的存储和提取 │
└─────────────────────────────────┘
更流程化地说:
客户端
↓
连接器
↓
SQL接口
↓
解析器
↓
优化器
↓
执行器
↓
存储引擎(InnoDB)
需要注意:MySQL 本身并不真正负责操作数据,真正操作数据的是存储引擎。
一条 SQL 是怎么执行的?
例如:
SELECT * FROM user WHERE id = 1;
MySQL 内部会经历:
1.连接器
写 SQL 之前,先要和连接器建立 TCP 连接。
登录 MySQL、建立连接、校验账号密码、权限验证,都是连接器负责。
由于后续这个连接里的操作都使用此时读取到的权限,所以修改权限后要重新连接才生效。
例如:
mysql -uroot -p
就是连接器在工作。
连接空闲超过 wait_timeout (默认是8小时)后会自动断开。
长连接 vs 短连接:
短连接:执行完几个SQL就断开,下次再重连。频繁创建连接开销大
长连接:连接复用,但MySQL临时使用的内存是绑定在连接对象上的,长连接累积可能导致内存暴涨(OOM),因为不会被立即释放
- 解决:定期断开长连接;或执行
mysql_reset_connection重置连接状态(MySQL 5.7+)
- 解决:定期断开长连接;或执行
2.查询缓存(8.0 已删除)
执行 SQL 前,先看这条 SQL 有没有之前查过、结果有没有缓存。Key 是 SQL 语句,Value 是结果。
有就直接返回,不走后面流程;现在 MySQL8.0 已经彻底删掉查询缓存。
任何对表的更新操作(INSERT/UPDATE/DELETE)都会清空该表的所有查询缓存。且对于写多读少的业务,查询缓存命中率极低,反而成了性能负担。所以 8.0 后彻底删掉了查询缓存。
需要注意的是:8.0 之后删除的是 查询缓存(query cache),InnoDB Buffer Pool 仍然在发挥缓存作用,缓存的是 ⌈ 页 ⌋ (数据页、索引页),而不是整条 SQL 结果
3.解析器
做两件事:
- 词法分析:识别关键字 select、from、where,分析这些关键字是什么意思
- 语法分析:判断 SQL 写得合不合法,语法错在这里直接报错
本质上就是将 SQL 转换为 MySQL 可以理解的结构。
注意:分析器只检查语法,不检查语义(比如表是否存在、列是否存在是在执行阶段或预处理阶段检查的)。
4.优化器
SQL 语句可能有多种执行方式,优化器选代价最小、速度最快的:
例如:
SELECT * FROM user
WHERE age = 20
AND name = 'Tom';
优化器会决定:
- 用哪个索引
- 多表关联顺序
- JOIN 顺序
- 是否走全表扫描
- 子查询是否转为半连接
也就是说,我们写的 SQL ≠ 真正的执行方式。真正的执行计划由优化器决定。这有点像编译器优化。注意,它选的其实不一定真的是最优的。
5.执行器
先判断对表有没有权限(连接器那一步获取的权限),如果有权限,再调用存储引擎(InnoDB)接口,真正去磁盘 / 内存一行行拿数据。
对于没有索引的查询,逐条读取并比对;有索引则按索引去查找满足条件的第一行,再下一条,直到不满足,然后返回结果给客户端。
存储引擎会决定是从 Buffer Pool 读页,还是从磁盘读页。
需要注意,执行器只是调用存储引擎接口,真正干活的是存储引擎,它真正负责数据存储、索引、事务、锁。MySQL 是老板,存储引擎是真正干活的人。
如果再详细一点,涉及到日志:
执行器 -> InnoDB引擎 -> 先写 redo log (prepare阶段)
-> 写 binlog
-> redo log commit
redo log(重做日志):InnoDB 独有的物理日志,用于崩溃恢复。采用 WAL(Write-Ahead Logging),先写日志再刷盘,保证不丢数据。redo log 是循环写。
binlog(归档日志):Server 层的逻辑日志,记录语句的原始逻辑,用于主从复制和数据恢复。binlog 是追加写。
两阶段提交:先让 redo log 处于 prepare 状态,再写 binlog,最后提交 redo log 。这样确保两个日志一致,崩溃恢复时能确定事务状态。
循环写和追加写具体是什么意思,下文会讲。
打个比方
- MySQL 服务层(连接器、分析器、优化器、执行器):公司老板 / 管理层
- InnoDB 存储引擎:仓库管理员
- 磁盘:真实仓库货架
- 内存 Buffer Pool:仓库门口的临时货架(缓存)
流程:
- 老板(执行器)说:帮我查 id=100 的数据
仓库管理员(InnoDB)自己决定:
- 先看门口临时货架(内存 BufferPool)有没有这条数据
- 有 → 直接从内存拿,快
- 没有 → 去真实仓库(磁盘)把数据页读出来,放到内存,再返回给老板
👉 注意:
**执行器只发指令,不碰磁盘、不管理内存缓存;
读内存还是读磁盘,全部是 InnoDB 自己内部搞定的。
再强调二者分工
1. 服务层(执行器)做什么
只干 3 件事:
- 经过优化器选定执行方案
- 调用 InnoDB 提供的接口:读一行、下一行
- 拿到数据后,做过滤、排序、聚合,返回给客户端
它完全不知道数据在内存还是磁盘,不关心物理存储细节。
2. 存储引擎(InnoDB)做什么
真正管事的:
- 管理磁盘上的数据文件、索引文件
- 管理内存缓冲池 Buffer Pool
- 决定要不要缓存数据、淘汰旧数据
- 处理事务、MVCC、行锁、redo/undo 日志
- 响应执行器的「拿数据」请求,自己从内存 / 磁盘取
- 负责事务一致性(ACID:Atomicity 原子性、Consistency 一致性、Isolation 隔离性、Durability 持久性)和数据完整性(外键、唯一约束等)
对于存储在磁盘文件的数据,InnoDB 会:
- 分页存储
- 使用缓存
- 使用 B+ 树管理数据(数据页通过 B+ 树索引组织,主键索引叫聚簇索引,二级索引叫非聚簇索引)
后面我们再复习:为什么数据库不 ⌈按行连续存储⌋
另外,为什么 MySQL 敢说自己快?因为它不是直接读写硬盘。
InnoDB 内存中的 Buffer Pool 发挥了作用:
读数据:先看内存里有没有,有就直接给,没有再从磁盘读并缓存。
写数据:先修改内存中的数据页(标记为脏页),同时记录
redo log,然后异步刷盘。
另外,InnoDB 所有数据、索引都按 16KB 一页组织管理
InnoDB 最小磁盘 IO 单元是页,默认大小 16KB。一条数据很小,不会一条一次磁盘 IO,而是一次性加载一整页到缓冲池。索引、行数据都是以页为单位组织存放在磁盘和内存中。
总结
执行器是发号施令的指挥官,InnoDB 是亲自跑腿、管内存管磁盘的实干者。
为什么是 InnoDB
1.InnoDB 支持事务:
例如:
BEGIN;
UPDATE account SET money = money - 100 WHERE id = 1;
UPDATE account SET money = money + 100 WHERE id = 2;
COMMIT;
所谓事务,就是要么全成功,要么全失败,不会在转账的时候出现你的账户少了100元,我的账户没有多100元的情况。
InnoDB 通过 undo log + redo log + binlog 保证事务原子性和持久性。
MyISAM 不支持。
2.InnoDB 支持行锁
例如:用户A修改 id=1,并不会影响用户B修改 id=2,并发相对高(但可能伴随竞态条件或死锁)。
而MyISAM是表锁,一个人改数据,整张表都可能被锁,并发较差。
注意,InnoDB是支持行锁+表锁,MyISAM 只支持表锁。
简单理解表锁和行锁:假设同时有两个用户执行下面这条 SQL:
UPDATE account SET money = money - 100 WHERE id = 1;
- InnoDB:
- 如果两条 UPDATE 修改不同 id(行),行锁会让它们同时执行,不会出现 money 减 200 的情况
- 如果修改相同 id,则第二条会等待第一条提交
- MyISAM:
- 整表锁,所以第二条 UPDATE 必须等待第一条完成
3.InnoDB 支持崩溃恢复
即使突然断电,数据也不容易丢,因为有redo log 重做事务、undo log 回滚事务,这个后续会在日志篇详细复习。
4.InnoDB 使用聚簇索引
暂时先记住:数据和索引在一起(InnoDB 主键索引存储数据页),后续在索引篇详细复习
- 优点:
- 主键查询非常快
- 范围查询顺序存储,有利于扫描
- 缺点:
- 插入顺序不对可能造成页分裂,写性能稍差
- 二级索引查主键可能需要回表
而 MyISAM 使用非聚簇索引(索引指向地址)
5.InnoDB 支持外键,MyISAM 不支持
总结
五个方面:
- 事务
- 锁粒度
- 外键
- 可靠性
- 索引结构
自测
Q1:一条SQL怎么执行?
A1:
连接器(用户认证、建立TCP连接) ->
查缓存(MySQL 8.0 已删除,缓存命中率太低) ->
解析器(识别关键字、分析语法错误) ->
优化器(类似编译器优化,处理“选哪个索引”“多表查询哪个表先查”“JOIN顺序”之类的问题) ->
执行器(调用存储引擎接口) ->
存储引擎对真实物理数据进行操作 ->
执行器组装结果集
Q2:什么是存储引擎?
A2:MySQL相当于公司老板,存储引擎相当于公司员工。老板有调用员工的资格,可以类比为MySQL有调用存储引擎的接口。存储引擎管理磁盘/内存中的数据(B+树数据结构)、执行来自执行器的CRUD请求、处理事务/锁/MVCC/redo log/undo log。总之,存储引擎是真正干活的。
Q3:为什么是InnoDB?
A3:
原因一:InnoDB支持事务,MyISAM不支持。
所谓事务,就是指一段SQL语句要么都被执行、要么都不被执行。典型的应用场景是转账。如果没有事务,可能会出现A的账户少了100元,但他的转账对象B的账户并没有多出100元的情况。
原因二:InnoDB支持行锁,而MyISAM使用表锁。
对于InnoDB,A能修改id=1,B也能同时修改id=2,并发能力强,但也伴随竞态问题与死锁问题;对于MyISAM,一段时间内只能一个人修改,因为整张表都被锁住了。
原因三:InnoDB支持崩溃恢复
redo log 重做事务,undo log 回滚事务。InnoDB 崩溃恢复主要依靠 redo log 的 WAL 机制(Write-Ahead Logging,预写日志,简称 WAL)。断电重启后,InnoDB 会检查 redo log,将已提交但未写入磁盘的数据重放(redo),将未提交的事务通过 undo log 回滚,保证数据一致性。
原因四:InnoDB支持聚簇索引
暂时先记住:相当于主键与数据存在一起,实际上是主键索引存储数据页。
原因五:InnoDB 支持外键,MyISAM 不支持。
Q4:当执行更新语句时,InnoDB 为什么要先写日志,而不是直接写磁盘?
A4:如果直接写磁盘,想象一个场景:
MySQL 的数据存在磁盘上的 .ibd 文件里。这个文件被分成很多个 16KB 的数据页。
当你执行 UPDATE users SET name = 'Tom' WHERE id = 100; 时:
用户
id=100的数据可能在磁盘文件的第 500 个页面。紧接着你执行
UPDATE orders SET status = 1 WHERE order_id = 999;。订单数据可能在磁盘文件的第 2000 个页面。
如果你直接写磁盘,磁头就要在磁盘上飞快地跳来跳去(寻道),寻找第 500 页,写一点;再寻找第 2000 页,写一点。这种跳跃式的写入是缓慢的。
如果是先写日志,InnoDB 不会立刻去改磁盘里的那个 16KB 页面,而是先把这次修改的操作记录在 redo log(重做日志)里。
redo log 是追加写的:它就像一个无限延长的账本,新的记录永远写在末尾。(注意:这和上文说的redo log 循环写 binlog 追加写不矛盾,待会会说)
那么,这究竟快在哪里呢?主要有三个方面:
1.将同步变为异步
在没有 redo log 之前,为了保证数据不丢,你必须在用户点击“提交”后,原地等待磁头跳到第 500 页把数据写完,才能返回“更新成功”。这个等待过程(同步写)是用户感知的卡顿。
有了 redo log 之后,写日志是顺序的,极快。写完日志,MySQL 就立刻给用户返回成功了。至于数据什么时候从内存刷到第 500 页,那是后台线程的事(异步写)。用户不再需要为磁盘磁头的物理跳跃而等待。
2.合并写入
虽然磁头最终要去写第 500 页和第 2000 页,但后台线程并不会来一个写一个,而是采取攒一波再写的策略:
- 场景 A(直接写):你在 1 分钟内对第 500 页修改了 10 次。磁头就要跳过去 10 次,写 10 次。
- 场景 B(WAL 模式):这 10 次修改在内存里完成,并顺序记入 redo log。后台线程发现第 500 页很“热”,它会等这 10 次修改都完成后,只跳过去一次,把最终结果写入磁盘。
这就把 10 次随机 IO 变成了一次随机 IO。
3.磁盘调度优化(IO 排序)
当后台线程决定要把内存中的脏页(被修改过的页)刷回磁盘时,它手里可能积压了几百个不同位置的写入任务(比如第 500 页、第 2000 页、第 800 页……)。
如果直接写,磁头的轨迹可能是:500 -> 2000 -> 800,来回折返。 但在异步模式下,MySQL 或操作系统可以对这些任务进行 排序,让磁头按顺序走:500 -> 800 -> 2000。 这就像快递员送货,如果来一单送一单,他在城里乱跑;如果把一天的单子拿在手里规划路线,他只需要按顺序绕城一圈。
Q5:redo log 是循环写还是追加写?
A5:补充说一下为什么上文说redo log是循环写,这里又说redo log是追加写。这里需要澄清一个概念:“追加写”是指一种 IO 行为模式,而“循环写”是指一种空间管理策略。
1.物理层面:redo log 和 binlog 都是追加写
无论是 redo log 还是 binlog,在将数据写入磁盘那一刻,磁头的动作都是**顺序追加
redo Log 的追加:虽然它在磁盘上表现为固定大小的一组文件(比如
ib_logfile0和ib_logfile1),但它内部维护了一个write pos(当前写入位置)指针。每次写入都是从write pos开始向后顺序写入binlog 的追加:它不断创建新文件(binlog.000001, binlog.000002...),也是一直向后追加
2.空间管理策略的差异
| 特性 | redo Log (重做日志) | binlog (归档日志) |
|---|---|---|
| 空间策略 | 循环写 (Circular) | 追加写 (Append) |
| 存储表现 | 空间固定。写满了会回到开头覆盖旧记录。 | 空间不固定。写满一个文件就开下一个。 |
| 覆盖前提 | 必须确保被覆盖的记录已经“落盘”(刷到数据页)。 | 永不覆盖,旧文件会一直保留直到被清理。 |
| 功能定位 | 崩溃恢复(保证最近的数据不丢)。 | 数据归档、主从复制(记录全量历史)。 |
总结:binlog 是“不覆盖的追加”,redo log 是“可覆盖的循环”。
3.为什么 Redo Log 要设计成循环写?
这是为了性能与空间的平衡。
redo Log 只是暂存:它的任务是保证 Buffer Pool 里的脏页在还没来得及刷入 .ibd 文件时,如果掉电了能救回来。一旦脏页被成功刷入了磁盘,这段 redo log 就完成了使命,变成了无用记录。
避免无限膨胀:数据库每天的修改量巨大,如果 redo log 不循环覆盖,它会迅速吞噬所有磁盘空间。
Checkpoint 机制:InnoDB 维护了一个 checkpoint 标志。write pos 追着 checkpoint 跑。如果 write pos 快要追上 checkpoint 了(表示空间满了),MySQL 就会强制停下来,把内存里的脏页刷一波到磁盘,然后把 checkpoint 往前推,腾出空间。
Q6:一条UPDATE语句和一条SELECT语句,在执行流程上最主要的区别是什么?涉及了哪些SELECT没有的组件?
A6:UPDATE 在 InnoDB 层需要写 redo log 保证崩溃恢复,在 Server 层写 binlog 用于主从复制和数据恢复,并且为了保证两个日志一致,需要两阶段提交。SELECT完全不涉及这些日志。
具体流程:
1.执行器调用引擎获取数据
2.引擎修改内存中的数据页,写 undo log(用于回滚和MVCC)
3.写 redo log,状态为prepare
4.执行器写 binlog
5.提交事务,redo log 状态改为 commit(两阶段提交完成)
Q7:假设 MySQL 在写 binlog 的过程中突然宕机了,此时 redo log 处于 prepare 状态,binlog 还没写完。恢复后,MySQL 会怎么处理这个事务?是提交还是回滚?为什么?
A7:MySQL恢复后,会把这个事务回滚。
原因:
两阶段提交的恢复规则是:如果 redo log 处于 prepare 状态,就去检查对应的 binlog 是否完整。
本题场景下,binlog 还没写完就宕机,所以 binlog 不完整。
这时为了保证 redo log 和 binlog 数据一致,就会回滚事务,undo 掉 redo log 中 prepare 状态的数据。
那如果回滚,redo log 的数据不就丢失了吗?对,就是要让它丢失。因为 binlog 里没有这个操作,如果提交了,主从复制时从库会缺少这个操作,导致主从数据不一致。一致性大于一切。
Q8:执行器在操作表之前还会做一次权限校验,为什么连接器已经校验过了,执行器还要再校验?
A8:有些SQL在分析器阶段,可能还不知道要操作哪些表(比如存储过程中的动态SQL、某些触发器场景)。分析器只做语法检查,不检查表级权限。所以执行器在真正打开表执行之前,必须再校验一次该用户有没有这个表的操作权限。执行器校验的其实是表级别的权限,比如有没有SELECT、INSERT权限等。连接器只管能不能连进来。