把"性能优化"拆成三张便签贴显示器上后,我终于不再把查询慢全怪给MySQL了
上周清理工位,从显示器边框撕下来三张泛黄的便利贴——"查缓存没""看N+1没""资源加hash没"。这三句话是我去年双十一被流量打崩三次后,给自己立的规矩。今天聊聊这张便签背后的真坑,不聊架构图那种虚的。
一、查询优化:别急着上ES,先把手里的LIKE放下
我站有个文章标签检索,早期直接 WHERE title LIKE '%关键词%',数据量到80万的时候搜索直接飙到1.8秒。当时第一反应是"上Elasticsearch",甚至都开了台2核4G的机器。后来冷静下来用 EXPLAIN 一看,全表扫描,filesort,一个不落。
实际改的却是另一套方案:把标题和摘要做了全文索引 FULLTEXT,搜索改成 MATCH(title,excerpt) AGAINST('关键词'),配合 BOOLEAN MODE 做精确匹配控制。同一条查询压到90ms,ES那台机器直接退了,省下的钱给CDN续了半年。
另一个隐蔽坑是"伪分页"。我后台有个用户列表,运营同事要导出"第1000页以后的数据"。LIMIT 100000,20 这种写法越往后越慢,因为MySQL还是要扫前面十万条。后来改成"上一页最大ID"的游标方式,WHERE id > 上次最大ID LIMIT 20,深度分页从秒级变毫秒级。代价是跳页功能没了,但运营说"本来也不跳页,都是往下翻"。
二、缓存:我因为"懒刷新"吃了两回亏
第一回是用户等级缓存。做了Redis缓存,但更新逻辑只删了主键缓存,忘了等级变更后要清"按等级筛选"的列表缓存。结果用户充了会员,前台列表里还是"普通用户"头像,客服被怼了两天才发现。
第二回更蠢。为了"性能"把配置表全量塞进一个Hash,HGETALL config 确实快。但某个配置项改了以后,我用的 HSET 单字段更新,同时另一个进程刚好在重建整个Hash。竞争条件下,新配置被旧数据覆盖,表现为"改完刷新又变回去"。后来改成配置项独立Key + 批量 MGET,牺牲一点点网络往返,换原子性。
现在我的缓存策略就两句话:写操作必须"先DB后缓存,删缓存而非更新缓存";读操作允许极短暂不一致,但关键业务(支付、权限)不走缓存直查DB。
三、静态资源:hash文件名救了我,但CDN刷新策略差点毁了我
前端打包加 content-hash 是基操了,我站JS/CSS都带8位hash。但去年踩了个CDN边缘节点的问题:源站更新了 app.a3f2b1c.js,CDN回源拿到新文件,可部分边缘节点因为"缓存键"配置里带了query string,而我又在URL后拼了个版本号 ?v=2,导致CDN把 app.a3f2b1c.js?v=1 和 ?v=2 当成两个资源,旧节点继续返回v1。
用户表现为"清缓存就好了",但普通用户不会清缓存。后来规范成:文件名本身带hash,URL不加任何query,CDN缓存键只认路径。版本更新靠文件名变化驱动,CDN自然回源。
图片懒加载也改过一轮。之前用 loading="lazy" 原生属性,但发现首屏大图还是被延迟了,因为浏览器把"首屏"算得很保守。现在首屏关键图直接写死src,非首屏用Intersection Observer手动控制,配合 srcset 按DPR出图。移动端2倍屏不再强拉原图,带宽省了四成。
四、一个没技术含量的习惯
我现在每次改完优化,必做一件事:把优化前后的 curl -o /dev/null -s -w '%{time_total}' 结果贴到对应代码注释里。三个月后回头看,能立刻知道这招还灵不灵,而不是靠记忆"应该变快了"。
三张便签撕了,换成一张新的:"优化不是一次性手术,是定期体检"。