首页 游戏 软件 排行 专题

缓存不更新的4类原因:版本号、刷新、TTL和预热各管一段

来源: 本站整理 日期:2026/11/3 13:25:34

上周有个客户找我,说后台已经改了样式,运营却在手机上看到的还是老页面。清缓存、清 Cookie、换浏览器、甚至用无痕模式打开,全都一样。第一反应都是“缓存”,于是在 CDN 后台点了一次刷新,说好了。结果过了半小时,还是旧页面。这类问题我一年要碰到好几次,根因其实不复杂:缓存这件事至少有三层,而多数人只在其中一层动了手,还以为三层都清了。

如果你正被“改了文件线上却没变”这件事折腾,或者想找一台海外节点做源站、把静态资源就近分发给用户,可以先看看这几家的报价和线路说明:cloudcone(全球云服务器)、hostwinds(全球云服务器),选源站之前先把线路和带宽算清楚,比事后救火省事得多。

所以这篇不打算教你“加个刷新按钮”,而是想把 CDN 缓存的层次关系捋顺,再按四类原因对号入座。看完你会发现,“缓存不更新”从来不是一个 bug,而是四个不同机制被混成了一件事。

浏览器缓存与CDN边缘节点工作关系示意图

先搞清楚:缓存到底有几层

一个静态资源从用户到源站,中间至少会经过三层缓存:

第一层是浏览器本地缓存,由响应头里的 Cache-Control / Expires 控制,你按 F5 通常只是发一个条件请求(If-Modified-Since),不一定真的重新下载;

第二层是CDN 边缘节点缓存,也就是各地区的边缘机房,每个节点各自存一份副本,刷新指令是逐节点异步下发的,有延迟;

第三层是源站应用自身的缓存,Nginx 的 proxy_cache、页面框架的模板缓存、甚至你的 OSS/COS 上给对象设置的元数据策略,都可能让“新的”响应其实是旧的。

三层的失效时间各不相同:浏览器看 Cache-Control,边缘节点看 TTL,源站看自己的缓存键。只动其中一层,其余两层照样把老内容端到用户面前。所以排查第一步永远是:先确认问题出在哪一层,别急着刷。

运维人员排查静态资源缓存不更新的工作场景

原因一:文件名没变,缓存键就没变

这是最常见、也最冤的一种。CDN 缓存的默认键就是 URL。你把 app.js 的内容改了,文件名还是 app.js,那么 CDN 认为“这个 URL 的东西没变”,边缘节点直接返回旧副本,根本不会回源。你在源站上检查文件确实是新的,但内容压根没机会传出去。

解决它的办法是让文件名跟着内容变:常见做法是构建时给文件加内容哈希,比如 app.9f2a1c.js;或者用 ?v=20261103 这种查询参数手工打版本。HTML 本身通常不缓存或只缓存很短时间,所以它引用的新文件名一发布,用户就自动拿到新资源,CDN 也不用刷新。这就是为什么主流前端框架默认都带 hash。

注意查询参数也是缓存键的一部分,所以 app.js?v=2 确实算新键。但别把版本号留在 URL 里长期不变地手改 手改一次容易忘一次,还是交给构建工具生成哈希更靠谱。

原因二:刷新指令发了,但没刷到你真正要的那个 URL

第二个坑出在刷新环节。很多人用的 CDN 控制台只有“刷新目录”或者“刷新文件”两种入口:刷新目录是 example.com/static/,刷新文件是 example.com/static/app.js。而真正被缓存住的可能是带查询串的 app.js?v=3,或者带签名的 OSS 直链。这类 URL 不在刷新路径下,指令跑完,边缘节点照旧返回旧副本。

另外几个容易被忽略的细节:刷新是异步的,点了以后节点生效需要几十秒到几分钟,你刷新完立刻用手机测,很可能测到的是还没生效的节点;刷新是按 URL 精确匹配的,URL 多了个斜杠、少个大小写,都算另一个键;如果站点开了 ignore query string 之类的忽略参数规则,那带参数的 URL 反而共用一份缓存,行为又不一样。

还有个更隐蔽的:有些 CDN 的刷新是有配额和优先级的,免费套餐里大目录刷新可能被降级处理,看起来“点了没反应”。上线前先确认你的套餐覆盖这个操作,比出事再解释快。

源站与CDN回源链路示意图

原因三:TTL 设得太长,边缘节点根本还没到期

第三类是压根没到刷新这一步。边缘节点上的副本还在有效期内,你没刷、也不该刷,它就是照规矩返回旧的。静态资源的 TTL 该设多少,取决于你更新频率和可接受的空窗期,常见做法是 HTML 设短(几十秒到几分钟,甚至 no-cache),带 hash 的 JS/CSS 设长(几个月甚至一年),图片同理。

这里必须把几个 Cache-Control 的值分清楚,混用是线上事故高发点:

max-age=3600 是“1 小时内可以直接用缓存,不用问服务器”;no-cache 不是“不要缓存”,而是“每次用之前必须先向服务器验证一次”,验证通过才用本地副本;no-store 才是真的不写任何缓存;must-revalidate 是过期后必须回源确认,不允许直接拿旧的顶。把这四个搞混,就会出现“我明明设了 no-cache,怎么还是旧的”这种迷惑现场。

还有一个点:HTML 缓存时间太长,是很多“发布后不生效”的隐形源头。用户拿到旧的 HTML,HTML 里引用的又是旧文件名的脚本,整个页面就冻在了上一版。哪怕脚本本身已经更新,用户也看不到。

文件版本号与缓存预热操作示意图

原因四:刷新了没预热,首访白屏比旧页面更糟

最后一类问题方向相反:不是旧的没换掉,而是新的没准备好。当你把一个目录刷新掉,大批边缘节点同时回源,源站如果配置一般(比如单台 2 核 4G 的机器、数据库没连接池、静态文件走 PHP 解析而不是 Nginx 直接吐),瞬间被打崩。这时候用户看到的不是旧页面,而是转圈甚至 502。

更糟的情况是回源失败被缓存成了错误响应,等 TTL 过期才恢复,用户白白等几分钟。应对办法有两个:一是预热,发布之后主动把关键 URL 提交预热任务,让节点提前把内容拉回本地,别等真实用户来当探针;二是给源站留余量,静态资源尽量直接由 Nginx 或对象存储返回,把源站带宽和并发算清楚,别让 CDN 的回源洪峰打在业务接口上。

顺带说一句,很多人在源站选型上只盯着“够不够便宜”,忘了算回源成本。CDN 回源流量是要从源站带宽里出的,如果源站只有 1M 端口,一次整站刷新就可能把带宽打满,后面所有动态请求一起变慢。这时候源站的带宽和线路质量,比 CPU 核数重要得多。

最后:一份能照着做的排查顺序

把上面的东西压缩成一条可执行的路径:

第一步,用 curl -I 看响应头,确认 Cache-Control、ETag、Last-Modified 到底是什么;

第二步,在响应头里找 CDN 回源标识(各家字段名不同,比如 X-Cache、Age),Age 是关键 Age 大说明这份是边缘节点的老副本,小说明刚回源;

第三步,带上 Cache-Control: no-cache 去请求同一个 URL,如果这时能拿到新内容,说明问题在缓存命中;如果还是旧的,那就是源站应用层自己返回的旧数据,跟 CDN 无关;

第四步,改文件名加 hash,让缓存键天然失效,这一步做完,八成问题当场消失;

第五步,发布流程里固定加两步:刷新精确 URL(注意带不带查询参数),刷新后做一次预热,再让前端同学用无痕窗口验证。

缓存这件事,说到底是分布式的状态一致性问题。链路上每多一层,就多一份不一致的可能。把层次分清、把键设计对、把发布流程写死,比一次次手动点刷新靠谱得多。

玩家留言 跟帖评论
查看更多评论
相关文章
猜你喜欢
同类下载