事情是这样的。
上周线上出事了,商品列表接口被人狂刷,QPS瞬间飙到三千多,数据库直接升天。监控报警响了一晚上,最后排查出来的原因让我一口老血喷出来——后端哥们儿把HTTP缓存头全注释掉了,理由是「怕线上数据不一致」。
我:???
怕数据不一致就把缓存全关掉,这逻辑就好比「怕出车祸所以把车砸了走路」。HTTP缓存是CDN的命根子,是后端扛并发的神器,你一句话就给阉了,CDN白买了吗?
所以今天来好好聊聊HTTP缓存,这玩意儿水很深,懂的人能把它玩出花,不懂的人只会Cache-Control: no-cache来回配置。
先搞清楚你是在跟谁对话
HTTP缓存不是铁板一块,它分两层:私有缓存和共享缓存。
浏览器缓存就是私有缓存,只属于你自己的浏览器,谁都碰不着。CDN、代理缓存就是共享缓存,一堆人共用一份资源。
这两个玩的东西完全不一样。很多新手把私有缓存的规则套到共享缓存上,结果CDN缓存失效,源站被打成筛子。
私有缓存:Cache-Control: private
共享缓存:Cache-Control: public
记住这句话:只有当数据足够「公开」,且对一致性要求不那么变态的时候,才值得上CDN缓存。用户个人数据就别想了,老老实实走私有缓存或者不放缓存。
强缓存和协商缓存,别再傻傻分不清
这是最让人晕的两个概念,我用大白话解释一下。
强缓存:浏览器看都不问服务器,直接从本地缓存里拿。服务器根本不知道你访问了。
协商缓存:浏览器先问问服务器,这东西有没有新版本?有就拿新的,没有就拿本地缓存的。
强缓存的响应头是这两个:
Cache-Control: max-age=3600
Expires: Thu, 21 May 2026 10:00:00 GMT
协商缓存靠这两个:
Last-Modified: Thu, 21 May 2026 08:00:00 GMT
ETag: "abc123"
重点来了——ETag是骨灰级玩家用的东西。Last-Modified只能精确到秒,遇到一秒内修改多次的情况就傻眼了。ETag是服务器根据文件内容生成的哈希值,内容变了他就变,精确到字节级。
但ETag有代价:每次请求都要去服务器验证。对于大文件或者高并发场景,这玩意儿可能比不用缓存还慢。所以Facebook这种量级的公司,早就抛弃了ETag,全靠Cache-Control撑场面。
Cache-Control不是一个值,是一整套指令
很多人以为Cache-Control就一个max-age,其实它是一个组合拳:
Cache-Control: public, max-age=31536000, s-maxage=604800, must-revalidate
让我拆解一下:
- public:响应可以被任何缓存存储,包括CDN和代理服务器
- max-age=31536000:浏览器缓存有效期一年(相对时间)
- s-maxage=604800:CDN缓存有效期七天(只对共享缓存生效,忽略max-age)
- must-revalidate:缓存过期后必须去服务器验证,不能拿过期的当最新用
看到没?CDN和浏览器可以设置不同的缓存时间!这是很多人不知道的骚操作。
一般套路是:源站缓存短,CDN缓存长。比如:
# 源站配置(nginx)
location /static/ {
expires 7d; # 浏览器缓存7天
add_header Cache-Control "public, s-maxage=2592000"; # CDN缓存30天
}
为什么要这样?因为静态资源更新的时候,你需要能及时推倒CDN的旧缓存。如果CDN缓存时间和浏览器一样长,那上线后用户得等一个月才能看到新版本,这显然不行。
Vary头——这个坑踩一次疼一周
如果你做过SSR或者API网关,Vary头你一定要懂。
假设你的API返回跟语言相关的文案:/api/products?locale=zh和/api/products?locale=en。如果你在CDN上缓存了中文版,然后有个英文用户来访问,CDN直接把中文结果返回给他——因为URL不一样,缓存key就不一样……不对,URL不一样,缓存key本来就不一样啊?
等等,如果CDN忽略查询参数呢?
# nginx配置忽略查询参数做缓存
proxy_cache_key "$host$uri"; # 默认不带$query_string
这种情况你就需要Vary头来救命:
Vary: Accept-Language, Accept-Encoding
这告诉缓存:缓存的时候要把这些请求头也考虑进去。同样是/api/products,中文浏览器和英文浏览器拿到的是两份不同的缓存。
这个坑我真踩过,当时有个接口返回的内容跟用户所在地区有关,上CDN后有用户反馈看到的是别人的内容,排查了大半天才发现是Vary没配。差点被祭天。
不要缓存的几种正确姿势
很多人以为「不缓存」就是Cache-Control: no-cache,nonono。
HTTP规范定义了一堆指令,用错场景你会死得很惨:
# 不缓存,但每次都去服务器验证(协商缓存)
Cache-Control: no-cache
# 完全不存储缓存,包括CDN和浏览器
Cache-Control: no-store
# 缓存但不使用,强制每次都去源站(HTTP/1.0兼容)
Pragma: no-cache
生产环境用得最多的是no-store,特别是涉及金钱、用户隐私、或者实时性要求极高的数据。如果你用了CDN,敏感数据一定要加no-store,不然CDN节点可能把你的数据泄露给别的用户。
至于no-cache,很多人误解成「不缓存」,结果配置了CDN还是被缓存了。实际上no-cache的意思是「缓存但每次验证」,CDN还是会存的。
一个实战场景:如何设计一个「可缓存但又不能太久」的API
这是最常见的需求:数据可以缓存,但不能超过一定时间,因为业务逻辑会变。
标准做法是用max-age+stale-while-revalidate:
Cache-Control: public, max-age=60, stale-while-revalidate=30
意思是:缓存60秒内直接用缓存,不用问服务器;60秒后120秒内,用户会拿到缓存数据,同时浏览器在后台发起验证请求更新缓存。
这样做的好处是什么?用户体验起飞——大部分用户拿到的都是缓存,没有网络延迟。但缓存不会太旧,最多「过期」了30秒业务就更新了。
这个头部在GitHub API和Cloudflare Workers里用得很多,是性能和一致性之间的完美平衡点。
最后说一句
HTTP缓存这个话题,说深了可以写一本书。但很多后端同学连Cache-Control都配不明白,更别说理解CDN和浏览器各自的行为差异了。
我见过把用户个人信息设成public缓存导致数据泄露的;也见过把静态资源设成no-cache让CDN形同虚设的;更见过Vary头忘配导致用户看到乱码内容的。
缓存不是洪水猛兽,它是性能优化的核武器。用对了,扛十万并发不用加一台服务器;用错了,直接把你的用户数据送到别人屏幕上。
下次有人跟你说「怕数据不一致所以把缓存关了」,你可以把这篇文章甩他脸上。
……开玩笑的,好好沟通,团队和谐最重要。
但真的,这东西值得每个后端工程师深入理解。