先给结论:多层缓存返回不同版本,通常不是某一层坏了,而是各层对同一请求的缓存键和失效信号理解不一致。定位顺序应是先固定一个可复现的请求,再逐层剥离缓存,确认差异出现在哪一层、由哪个响应头或配置触发。下面用一个假设情境说明判断过程。
假设某站点把 www 域名接入 CDN,源站前还有一层反向代理缓存。运维发现首页在浏览器刷新后仍是旧版,但直接访问源站 IP 得到的是新版。此时不能直接判定 CDN 缓存未刷新,因为反向代理可能已经把旧版又写回了自己的缓存。更合理的做法是先确认每一层各自看到什么,再决定清理哪一层。
如果源站 IP 返回新版、反向代理回源地址返回旧版,说明旧内容停留在反向代理层,CDN 只是忠实缓存了它拿到的结果。反过来,如果反向代理返回新版、CDN 边缘仍返回旧版,则问题在 CDN 的缓存键或刷新范围。这个区分直接决定下一步动作:前者要修正反向代理的失效逻辑,后者要检查 CDN 的缓存规则和刷新提交。
第一步不是清缓存,而是记录一个完整请求:完整 URL、Host 头、请求方法、Cookie 是否存在、Accept-Encoding、以及是否带查询参数。多层缓存常因这些字段不同而落到不同缓存键上。同一路径带与不带尾斜杠、带与不带跟踪参数,可能被当成两个对象分别缓存。
第二步按层剥离。可以直接请求源站,绕过所有缓存;再请求反向代理,绕过 CDN;最后请求 CDN 边缘。每层记录响应状态、关键响应头和内容摘要。若某一层返回的 Age 明显偏大,只能说明该层命中了缓存,不能说明它命中的是新版还是旧版,仍需比对内容。
第三步比较响应头中的缓存指令。重点看 Cache-Control 的 max-age、s-maxage、no-cache、private,以及 Vary 和 ETag。如果源站对同一 URL 根据 Cookie 或设备返回不同 HTML,却没有设置合适的 Vary,中间层就可能把 A 版本缓存后返回给需要 B 版本的请求。这属于缓存键设计问题,不是缓存没刷新。
实际动作示例:在反向代理层临时关闭缓存,只让 CDN 回源。如果此时 CDN 刷新后返回新版,说明问题集中在反向代理的失效逻辑;如果仍然返回旧版,则要继续检查 CDN 的缓存键和刷新任务是否覆盖了带参数的 URL。这个动作的结果会直接缩小排查范围,避免同时清理多层而无法判断哪一层真正生效。
第一种是缓存键差异。同一逻辑页面因协议、www 前缀、尾斜杠、查询参数、Cookie、语言头不同,被拆成多个缓存对象。旧版本可能只存在于其中一个键下。判断依据是:改变其中一个字段后,返回版本是否变化。若变化,优先统一缓存键策略或做规范化跳转。
第二种是失效信号未贯通。源站更新后只通知了 CDN,却没有通知反向代理;或者反向代理刷新成功,但 CDN 的刷新任务因 URL 不匹配而漏掉。判断依据是各层 Age 与内容版本不一致,且手动逐层刷新后能短暂一致。这里要注意,刷新成功不等于长期一致,还要确认发布流程是否会稳定触发每一层的失效。
第三种是回源结果本身不稳定。源站有多台后端,其中一台仍挂着旧代码或旧数据;缓存层只是把不同后端的响应分别缓存了。判断依据是绕过缓存多次请求源站,返回版本是否随机变化。若是,问题不在缓存,而在源站发布一致性。
这三种原因的证据不同,处理动作也不同。缓存键问题靠统一规则;失效问题靠打通发布链路;源站不一致靠发布原子性和健康检查。把它们混在一起,就容易反复清缓存却无法根治。
当旧内容、旧系统或旧合作关系需要退出,而其中一部分仍有价值时,缓存一致性排查会多一个约束:不能简单地把整层缓存关掉。更稳妥的做法是先冻结入口,把仍有价值的路径迁移到新规则下,再对旧路径设置明确的失效和跳转策略。
具体判断可以按以下顺序:
一个可操作的动作是:对准备退出的旧路径设置短缓存或直接不缓存,同时保留源站可回源。这样即使旧系统仍在运行,也不会因为长缓存把旧版本固定住。若观察一段时间后旧路径请求量下降,再决定是否彻底下线。请求量下降本身不能单独证明退出正确,还要确认没有外部依赖仍在调用。
定位完成后,验证不能只看浏览器。至少要在同一时间点分别请求源站、反向代理和 CDN 边缘,比对内容摘要和关键响应头。若三层一致,再改变一个变量,例如带与不带查询参数、带与不带 Cookie,确认缓存键策略符合预期。
如果站点使用 robots.txt 限制抓取,要清楚它只约束爬虫行为,不等于可靠的索引移除;站点地图也不保证收录。这些手段与缓存一致性是不同层面的问题,不能用它们替代缓存层的排查。HTTPS 同样不保证内容版本一致,它只解决传输加密。
最终判断标准是:同一逻辑页面在约定条件下返回同一版本,且发布后各层能在可接受时间内收敛。若仍不一致,回到请求固定和逐层剥离的步骤,重新确认差异出现在哪一层。