13人参与 • 2026-08-06 • Mysql
mysql架构只有两层,但大多数性能问题都发生在层间交互处:
┌─────────────────────────────────────────────────────────────┐
│ mysql server层(通用逻辑) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │connector│→│ parser │→│optimizer│→│ executor│ │
│ │连接/权限│ │词法/语法│ │执行计划│ │调用引擎│ │
│ └─────────┘ └─────────┘ └─────────┘ └────┬────┘ │
│ │ │
│ ┌──────────────────────────────────────────┘ │
│ │ binlog(逻辑日志,server层维护,所有引擎共享) │
│ └───────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
│
▼ 引擎api(handler接口)
┌─────────────────────────────────────────────────────────────┐
│ 存储引擎层(innodb) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │buffer │→│undo log │→│redo log │→│ 磁盘文件 │ │
│ │pool │ │(回滚) │ │(wal) │ │(ibd/data)│ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────────┘
关键认知:server层只负责"怎么执行",innodb负责"数据在哪"。优化器选错索引?这是server层的锅。主从延迟?可能是binlog和redolog的协作问题。oom崩溃?大概率是buffer pool或连接内存管理的问题。
连接器在tcp握手后做两件事:认证身份、查询权限表并缓存到连接对象。这意味着:
-- 场景:管理员 revoke 了某用户的delete权限 revoke delete on db.* from 'app_user'@'%'; -- 但已建立的连接不受影响,直到重连 -- 这在生产环境曾导致"权限已回收但数据仍被删"的事故
⚠️ 生产建议:修改权限后,务必执行
kill <thread_id>断开已有连接,或等待wait_timeout(默认8小时)后自然失效。
mysql的连接内存不是线程池模式,而是每个连接独立分配:
连接内存 = 会话级变量 + 临时表 + 排序缓冲区 + 二进制日志缓存 + ...
当使用连接池(如hikaricp)保持长连接时,如果执行过大查询(如 select * from huge_table order by),排序缓冲区可能膨胀到数十mb。语句执行完毕后,这些缓冲区会被标记为空闲并在本会话的后续查询中复用,但从操作系统视角(resident memory)来看,内存并未真正归还给os,而是保留在线程的内存池中。因此长连接累积的"内存池占用"会持续增加,直到连接断开才释放。citeweb_search:2#0
解决方案:
mysql_reset_connection() 重置连接状态(无需重连,权限不变)maxlifetime(hikaricp默认30分钟),强制轮换连接show processlist 中 memory 列,异常增长的连接需要排查查询缓存的kv设计(key=sql文本,value=结果集)看似美好,实则存在结构性缺陷:
| 问题 | 根源 | 影响 |
|---|---|---|
| 失效成本极高 | 任何写操作(insert/update/delete)会清空整张表的所有缓存 | 写多读少场景缓存命中率趋近于0 |
| 全局锁竞争 | 缓存维护需要全局互斥锁 | 高并发下成为性能瓶颈 |
| 判断逻辑粗糙 | 只要sql文本有差异(空格、注释、大小写)就视为不同key | 缓存碎片化严重 |
💡 替代方案:将缓存上移到应用层(redis/memcached),或利用innodb的buffer pool(天然缓存数据页,不受写操作全量失效影响)。
分析器从 information_schema 读取表结构进行元数据校验。在表数量庞大的实例中,这会成为瓶颈:
-- 查看分析阶段耗时(mysql 8.0+) select * from performance_schema.events_stages_history_long where event_name like '%sql/parse%';
优化器基于**成本(cost)**选择执行计划,但成本估算可能严重偏差:
-- 案例:优化器误判索引选择 explain select * from orders where user_id = 100 and create_time > '2024-01-01'; -- 可能选择 idx_user_id,但实际 idx_create_time 更高效(时间范围过滤更严格)
优化器局限:
💡 调优工具:
explain analyze(mysql 8.0.18+)显示实际执行时间,比传统explain更准确。
执行器在调用引擎前做最终权限校验(precheck无法覆盖触发器等运行时对象)。但真正的性能博弈在引擎层:
buffer pool不是简单的lru,而是改进版lru(midpoint insertion):
┌─────────────────────────────────────────────────────────────┐ │ young区(热数据,约5/8) │ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │ a │→│ b │→│ c │ ... │ x │→│ y │ │ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │ │ ↑ ↑ │ │ 频繁访问 midpoint │ │ │ │ old区(冷数据,约3/8) │ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │ m │→│ n │→│ o │ ... │ z │→│ │ │ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │ │ ↑ ↑ │ │ 观察期(默认1秒) 淘汰尾部 │ └─────────────────────────────────────────────────────────────┘
核心机制:新读入的页不直接放入头部,而是插入midpoint(冷区头部)。只有在old区度过观察期(innodb_old_blocks_time,默认1000ms)且再次被访问,才会晋升到young区。citeweb_search:2#4
这解决了全表扫描的缓存污染问题:扫描时顺序读取的页在old区,如果不再被访问,很快被淘汰;真正的热数据在young区不受影响。
buffer pool通过三个链表管理页:citeweb_search:2#0
| 链表 | 职责 | 关键操作 |
|---|---|---|
| free list | 管理空闲页 | 启动时所有页在此,分配时移除 |
| lru list | 管理使用中的页(含脏页和干净页) | 访问时移动位置,淘汰时释放 |
| flush list | 管理脏页(按修改lsn排序) | 后台线程定期刷盘,保证checkpoint推进 |
脏页刷盘策略:
innodb采用wal(write-ahead logging):先写redo log,再刷脏页。redo log是物理日志,记录"在某个数据页上做了什么修改",采用循环写入(固定大小,如4个1gb文件)。
write pos
↓
┌────────┬────────┬────────┬────────┐
│ib_log │ib_log │ib_log │ib_log │
│file_0 │file_1 │file_2 │file_3 │
└────────┴────────┴────────┴────────┘
↑
checkpoint
redo log先写入内存的redo log buffer(默认16mb),再按策略刷盘:citeweb_search:2#8
| innodb_flush_log_at_trx_commit | 行为 | 安全性 | 性能 | 适用场景 |
|---|---|---|---|---|
| 0 | 每秒刷盘 | 低(可能丢1秒数据) | 最高 | 非核心日志、监控数据 |
| 1 | 每次事务提交同步刷盘 | 最高 | 低 | 金融交易、订单系统(推荐) |
| 2 | 写入os缓存,每秒刷盘 | 中(os崩溃可能丢数据) | 中 | 一般业务 |
⚠️ mysql 8.0变化:redo log写入改为多线程异步架构(log_writer、log_flusher、log_closer)。
版本权衡:5.7的单锁模型在低并发时延迟更低(无线程切换开销);8.0的多线程模型在高并发时吞吐量更高(锁竞争分散)。两者各有优劣,需根据实际负载选择——低并发核心系统可保留5.7,高并发业务推荐8.0。citeweb_search:2#15
binlog是逻辑日志,记录sql语句的原始逻辑(如"给id=2的c字段加1"),采用追加写入,不覆盖历史日志。
| 维度 | redo log | binlog |
|---|---|---|
| 层级 | 存储引擎层(innodb特有) | server层(所有引擎共享) |
| 内容 | 物理日志(页修改) | 逻辑日志(sql语句) |
| 写入方式 | 循环写 | 追加写 |
| 用途 | 崩溃恢复(crash recovery) | 主从复制、数据恢复、审计 |
| 参数 | innodb_flush_log_at_trx_commit | sync_binlog |
redo log和binlog是两个独立的系统,如果不用2pc:
场景a:先写redo log,后写binlog
场景b:先写binlog,后写redo log
阶段一(prepare): ├─ 引擎将更新记录到redo log,标记为prepare状态 └─ 告知执行器:随时可以提交 阶段二(commit): ├─ 执行器生成binlog并写入磁盘 └─ 执行器调用引擎提交接口,redo log改为commit状态
崩溃恢复规则:
现象:监控告警 seconds_behind_master 从0秒突增到4784150秒(约55天),执行的是 delete from table(仅50万数据)。citeweb_search:2#7
排查:
-- 从库查看 show slave status\g; -- seconds_behind_master: 4784150 select * from information_schema.innodb_trx\g; -- trx_state: running -- trx_query: delete from wggl_sjgdxq -- trx_rows_modified: 136799 -- 发现是一个大事务在从库单线程执行
根因:
slave_parallel_workers=0)串行回放解决:
slave_parallel_workers=4,slave_parallel_type=logical_clockinnodb_io_capacity 匹配硬件现象:mysql每2-3天被系统oom killer杀掉,重启后正常。
排查:
-- 查看连接内存占用 select id, user, host, db, command, time, max_memory_used/1024/1024 as mem_mb from performance_schema.threads order by max_memory_used desc; -- 发现部分连接内存占用超过500mb
根因:
order by、group by 操作sort_buffer_size)和临时表内存(tmp_table_size)未释放maxlifetime,连接永久存活解决:
maxlifetime=1800000(30分钟)sql_big_result 提示,避免内存临时表mysql_reset_connection()(mysql 5.7+)现象:凌晨备份任务后,白天业务高峰期buffer pool命中率从99%跌至85%,qps下降40%。
排查:
-- 查看buffer pool状态 show engine innodb status\g; -- pages made young: 突然激增 -- buffer pool hit rate: 从1000/1000降至850/1000 -- 查看是否全表扫描 select * from performance_schema.events_statements_history_long where sql_text like '%select%backup_table%';
根因:
select * from huge_table(全表扫描)innodb_old_blocks_time=0(被误改)解决:
innodb_old_blocks_time=1000(默认1秒观察期)select sql_no_cache(虽然查询缓存已移除,但可显式避免其他缓存干扰)innodb_buffer_pool_size 的扫描影响| 参数 | mysql 5.7建议 | mysql 8.0建议 | 作用 |
|---|---|---|---|
innodb_buffer_pool_size | 物理内存50-70% | 物理内存50-75% | buffer pool大小 |
innodb_buffer_pool_instances | ≥1gb时8个 | ≥1gb时8个 | 减少锁竞争 |
innodb_flush_log_at_trx_commit | 1(金融)/2(普通) | 1 | redo log刷盘策略 |
sync_binlog | 1 | 1 | binlog刷盘策略 |
innodb_old_blocks_pct | 37 | 37 | old区占比(3/8) |
innodb_old_blocks_time | 1000 | 1000 | 晋升观察期(ms) |
innodb_lru_scan_depth | 1024 | 1024 | lru扫描深度 |
slave_parallel_workers | 4-8 | 4-8 | 从库并行复制线程 |
slave_parallel_type | logical_clock | logical_clock | 并行复制类型 |
| 特性 | mysql 5.7 | mysql 8.0 |
|---|---|---|
| 查询缓存 | 存在(建议关闭) | 已移除 |
| redo log架构 | 单线程写入 | 多线程异步(log_writer/flusher/closer) |
| 默认字符集 | latin1 | utf8mb4 |
| 降权索引 | 不支持 | 支持(invisible index) |
| explain | 传统格式 | 支持explain analyze(实际耗时) |
查询语句: 客户端 → 连接器(认证+权限缓存) → 查询缓存(5.7存在,8.0已移除) → 分析器(词法/语法/元数据校验) → 优化器(成本模型选索引) → 执行器(权限校验+调用引擎) → innodb(buffer pool命中?→ 返回/读磁盘) → 返回结果集 更新语句: ... → 执行器 → innodb: ├─ 读取数据页(buffer pool/磁盘) ├─ 记录undo log(用于回滚/mvcc) ├─ 修改buffer pool数据(标记脏页,加入flush list) ├─ 写入redo log buffer → 刷盘(prepare状态) ├─ 执行器生成binlog → 写入磁盘 └─ redo log改为commit状态(两阶段提交完成) → 返回更新结果
核心设计哲学:
“理解mysql的执行流程,不是记住每个组件的名字,而是理解每个设计决策背后的权衡——性能 vs 一致性、内存 vs 磁盘、复杂度 vs 可靠性。”
本文基于mysql 5.7/8.0架构原理整理,参考mysql官方文档、innodb源码及生产故障案例。
到此这篇关于mysql执行流程原理深度解析的文章就介绍到这了,更多相关mysql执行流程原理内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论