MySQL 查询从 2.3s 压到 12ms 的那次凌晨三点,我重新认识了"慢"的定义

站长杂谈 25 浏览 0 回复 返回上级

上周有个老站突然告警,CPU 飙到 90% 以上。爬起来看监控,慢查询日志里一条统计报表的 SQL 跑了 2.3 秒,并发一上来直接拖垮。这表 80 万行数据,说大不大,说小也不小。折腾到凌晨三点,最后压到 12ms,记录一下这次"性能小抄"的几个关键点。

一、查询优化:别急着上缓存,先看看索引是不是在"装死"

那条慢 SQL 是个典型的问题:SELECT 里套了子查询做 COUNT,外面再 GROUP BY 日期。EXPLAIN 一看,type 是 ALL,Extra 里 "Using temporary; Using filesort" 全齐活了。

我的习惯是先抓执行计划,而不是先想 Redis。这次把子查询拆成 JOIN,给 WHERE 里的时间字段和 GROUP BY 字段补了个联合索引 (created_at, status),顺序调了两次才命中。MySQL 8.0 的降序索引确实省了点事,但最核心的是把"先聚合再过滤"改成了"先过滤再聚合",数据量从 80 万降到 800 行。

有个细节:联合索引 (status, created_at) 和 (created_at, status) 在这个场景下差别巨大。因为 status 是低基数字段(就 4 个值),放前面会导致索引选择性极差。用 SHOW INDEX 看了下 Cardinality,确认 created_at 放前面才对。

二、缓存层:我把它从"救命稻草"改成了"锦上添花"

之前这站到处塞 Redis,模型查询缓存、页面片段缓存、全页缓存三层叠加,结果缓存穿透 + 雪崩一起来的时候,MySQL 压力比没缓存还大。

这次重构后定了条规矩:缓存只存"计算结果",不存"原始数据"。那条 12ms 的 SQL 本身够快了,但报表页面还要做同比环比计算,这部分用 Redis 存了 5 分钟。键名加了业务前缀和版本号,比如 stats:dashboard:v2:20240831,方便哪天逻辑变了直接换前缀淘汰。

另外给热点 key 加了随机 TTL,原来全是 300 秒整,每到整点缓存同时失效,DB 连接数直接尖刺。现在 240-360 秒随机,曲线平滑多了。

三、静态资源:CDN 不是万能药,回源配置错了比不用还慢

顺手把前端也捋了一遍。这站用了 OSS + CDN,但发现首屏加载还是 1.8s。Chrome DevTools 里一看,十几个 JS 文件虽然走了 CDN,但每个都带了个 ?v=xxx 的查询参数,CDN 节点全部回源,命中率不到 30%。

改成文件名哈希(app.a3f2b1c.js 这种),配合 Nginx 的 try_files 做永久缓存。图片用了 WebP 自适应,Nginx 配置里加了个 map 判断 Accept 头,老浏览器 fallback 到 JPEG。这个配置我贴出来,有需要的自取:

map $http_accept $webp_suffix {
  default "";
  "~*webp" ".webp";
}
location ~* \.(jpg|jpeg|png)$ {
  add_header Vary "Accept";
  try_files $uri$webp_suffix $uri =404;
}

还有一个坑:CDN 的 HTTP/2 Server Push 我开了一段时间又关了。Push 的资源如果浏览器缓存里已有,会浪费带宽;而且 ThinkPHP 的模板渲染是动态的,很难精准控制 Push 时机。最后改成 preload 链接头,让浏览器自己决定要不要提前取。

四、一个反直觉的发现

优化完上线,总耗时从 2.3s 降到 180ms(含前端),但服务器 CPU 反而降了 60%。之前一直以为慢查询只是"慢",没想到它还会引发连接池耗尽、PHP-FPM 进程积压、Nginx 502 连锁反应。性能优化有时候救的不是速度,是稳定性。

现在我的 checklist 里多了三条:新功能上线前必跑 EXPLAIN,缓存键必带版本号,静态资源必查 CDN 命中率。不是什么高深技术,就是踩过坑后长出的条件反射。

你们有没有类似"以为是小问题,结果牵出一串"的经历?欢迎聊聊。

评论0
回复 · 0
还没有回复
微信客服 微信客服