MySQL 查询缓存关掉之后,我才开始真正理解"缓存该放在哪一层"

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
站长杂谈 59 浏览 0 回复

上周把一台老站从 MySQL 5.7 迁到 8.0,升级完发现查询反而慢了。查了半天才想起来:8.0 直接把查询缓存干掉了。之前那个 query_cache 虽然命中率常年不到 30%,但好歹是个心理安慰,现在连安慰剂都没了。

被迫重构的时候,我把缓存策略从头捋了一遍,发现以前简直是"哪里慢塞哪里",根本没过脑子。现在这套分层思路用了两个月,分享一下,顺便也给自己留个档。

第一层:连接池与持久连接别混着用

PHP-FPM 环境下,pdo_mysql 的持久连接(PDO::ATTR_PERSISTENT)和连接池是两回事。我之前图省事开了持久连接,结果 MySQL 端出现大量 Sleep 状态连接占满 max_connections,高峰期新请求直接连不上。现在的做法:普通查询走短连接,批量任务走独立的长连接池,中间用 Swoole 的连接池做隔离。代价是多维护一份配置,但连接数从经常飙到 900+ 降到了稳定在 120 左右。

第二层:业务缓存别偷懒用 Redis 存 SQL 结果

以前我的套路:复杂 SQL 跑完,把整个结果数组 json_encode 塞 Redis,key 里拼上 SQL 的 md5。看起来省事,实际上缓存粒度太粗,任何一张关联表更新都要整个失效,命中率惨不忍睹。现在改成缓存"业务对象":用户基础信息、栏目结构、配置项这些变动频率差异大的东西分开存,用 tag 批量失效。一个列表页可能组装自三四个缓存 key,但更新时只需要清掉变的那部分。

第三层:OPcache 预热被我忽略了两年

PHP 8 的 OPcache 默认配置对频繁部署的站点很不友好。我之前每次发版后第一个请求必卡 2-3 秒,就是因为文件重新编译。现在上线脚本里加了一步:用 wget 顺序访问核心接口做预热,同时把 opcache.revalidate_freq 从 2 改成 60,开发环境单独走另一套配置。这个小改动让发版后的 P99 响应时间从 2800ms 降到了 400ms 以内。

第四层:静态资源我差点被 CDN 边缘节点坑了

图片和 JS/CSS 走 CDN 是基操,但我之前犯了个蠢:文件名带 hash 的打包资源,CDN 缓存时间设了 30 天,而 HTML 页面缓存 5 分钟。结果某次发版后,老 HTML 引用了新 hash 的资源,边缘节点还没同步到,用户页面白屏了十几分钟。现在的规则:HTML 不缓存或极短缓存,带 hash 的资源永久缓存,不带 hash 的(比如上传的图片)用版本号 query 控制,CDN 回源策略统一改成"优先回源"。

第五层:浏览器缓存不是设了就行

Chrome 的 disk cache 在移动端表现很诡异。我监控到部分安卓用户每次打开都重新下载 2MB 的 JS 包,后来发现是 Cache-Control 里同时写了 max-age 和 no-cache,某些浏览器解析优先级不一致。现在统一用 etag + last-modified,大文件额外开 br 压缩,首屏关键 CSS 直接内联。Lighthouse 性能分数从 62 蹭到了 91,虽然这个分数仅供参考,但用户侧的实际加载时间确实降了。

最后说个反直觉的

我之前痴迷于用各种缓存把响应压到 50ms 以内,结果监控发现:用户从点击到看到内容,80% 时间花在 DNS 解析和 TLS 握手上。后来把 DNS 切到支持 HTTPDNS 的解析商,TLS 1.3 强制开启,0-RTT 能开的开,这些"网络层"优化比我在代码里抠 10ms 有效得多。

现在我的原则:先测瓶颈在哪,再决定优化哪一层。别像我以前,看见慢就上 Redis,最后缓存比数据库还忙。

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