你的接口慢成狗,可能只是因为缓存没整明白
见过太多这样的场景:系统一上线,后端同学对着监控大屏发呆——CPU不高、数据库正常、Redis也活着,但接口响应就是慢得像在等公交车。
然后一顿操作猛如虎:加机器、加索引、Redis集群……结果呢?该慢还是慢。为啥?因为方向就没整对。
今天说个被严重低估的问题——HTTP缓存。这东西说实话,很多工作三年的后端都不一定整明白了。
先搞清楚一个核心问题:谁的缓存?
HTTP缓存分两层:客户端缓存(浏览器、App)和中间层缓存(CDN、代理服务器)。很多开发者只知道后端要加缓存,却忽略了HTTP协议本身给你留的那些机制。
这两层配合好了,能把90%的请求直接拦截在服务端之外。加机器都解决不了的问题,缓存能。
Cache-Control头:你可能一直在瞎写
大多数后端同学返回的Cache-Control头,基本都是复制粘贴来的——或者直接返回个no-cache图省事。
来,看看你有没有踩过这些坑:
// 坑1:所有接口都no-cache,浏览器每次都去验证,浪费
res.setHeader('Cache-Control', 'no-cache');
// 坑2:max-age设成0,等于没缓存,每次都重新拉
res.setHeader('Cache-Control', 'max-age=0');
// 坑3:只设了public,却没考虑私有数据泄露问题
res.setHeader('Cache-Control', 'public, max-age=3600');
Cache-Control其实是有一套组合拳的:
max-age=3600:告诉浏览器,这个响应可以缓存3600秒,期间直接用,不来问我no-cache:每次用之前都得问我(服务器),但不一定重新下载no-store:彻底不要缓存,真·每次都拉新的privatevspublic:private只能存在用户本地,public可以存在CDN等中间节点
这里最容易搞混的是no-cache和no-store。no-store是真的不存任何缓存副本,适合处理敏感数据。no-cache是存了,但每次得跟服务器确认一下——这对于需要实时数据的场景是合理的。
ETag和Last-Modified:你根本没用上的好东西
有的接口内容变一次,整个资源就变了。这时候用ETag或者Last-Modified就非常合适。
ETag本质上是一个版本标识符——服务器给资源打一个tag,变了就换tag。浏览器再次请求时,把这个tag带过去(If-None-Match头),服务器一看——哦,tag没变,返回304 Not Modified,浏览器继续用本地缓存。
// 服务器端示例(Node.js/Express风格)
app.get('/api/config', (req, res) => {
const data = getConfigData();
const etag = generateEtag(data); // 用数据内容的hash生成ETag
if (req.headers['if-none-match'] === etag) {
return res.status(304).end(); // 没变化,告诉浏览器用缓存
}
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
res.json(data);
});
304响应体是空的,网络传输量几乎为零。这比每次都返回完整的JSON要强太多。
很多后端根本不返回ETag,白白浪费了这个机制。有的甚至每次都生成新的ETag——那跟没缓存有什么区别?
CDN缓存:很多人第一反应就是"清理缓存真麻烦"
CDN缓存确实是把双刃剑。用好了能把接口响应时间从200ms压到5ms,用不好就是用户看到过期数据的噩梦。
我见过有人因为害怕缓存问题,干脆把CDN关了——这是因噎废食。
正确姿势是分层处理:
- 静态资源(JS/CSS/图片):CDN缓存一年都行,用文件名hash来控制版本
- 用户无关的公共数据(配置、字典表):CDN缓存, TTL设置合理范围
- 用户私有数据:绝对不要上CDN,用private缓存
- 实时性要求高的数据:用swr或请求合并,不要硬刚缓存
至于清理问题——你都有版本控制工具了,为啥不把缓存也做成有版本的概念?URL加个版本参数,或者用文件名hash,都能优雅解决。
一个真实的反例:那个被缓存坑死的活动页
说个我见过的真实案例。某电商平台做秒杀活动,商品信息接口设置了CDN缓存TTL=300秒(5分钟)。
结果活动一开始,商品库存从100变成了0,但用户刷了5分钟页面,看到的还是"有货"。
后端的锅吗?不一定。是缓存策略设计的时候就埋下的雷——库存这种高频变更的实时数据,就不应该用CDN做长时间缓存。
正确的做法是:库存接口走私有缓存(或者no-cache),或者前端轮询+请求合并,把实时性和性能做个平衡。
强缓存 vs 协商缓存:什么时候用哪个?
很多人搞不清楚这两个概念的区别。
强缓存:本地直接用,不问服务器。Cache-Control的max-age和Expires头就是干这个的。
协商缓存:每次用之前问一下服务器,服务器说能用就用,不能用就下载新的。ETag/Last-Modified + If-None-Match/If-Modified-Since这套组合。
选哪个?看数据更新频率:
- 几乎不变的数据(logo、静态配置):强缓存,max-age设长一点
- 经常变化但不要求实时(排行榜、日榜):强缓存+版本号,或者短TTL的强缓存
- 必须实时(订单状态、库存):协商缓存或者不缓存
写在最后
HTTP缓存不是玄学,也不是加个Redis就完事了的事。它是HTTP协议层面就定义好的一套机制,用好了能四两拨千斤。
下次系统慢了,别急着加机器。先看看HTTP缓存有没有用到位。说不定,加了反而是浪费。
当然,如果你把Cache-Control设成了no-store然后问为啥慢——那问题就不在缓存了,在脑子。