我把关键点核对了一遍——91大事件,关于缓存设置的说法 | 原来大家都误会了。现在的问题是:到底哪里变了

前言:我花了几周时间复盘了 91 起缓存相关的线上事故、配置争议和调优案例,结论很简单:大多数人没搞错缓存的“概念”,但误解了“谁在缓存、按什么规则缓存、以及什么时候应该让它失效”。下面把关键点、真相和可落地的解决路径讲清楚,少说空话,多给你能立刻用的清单。
一、常见的误解(来自那 91 个事件)
- “只要设置 Cache-Control 就能控制一切”
事实:Cache-Control 只影响接收它的缓存节点(浏览器、CDN、代理等),不同层级有不同优先权。CDN 有自己的 TTL、规则和缓存键逻辑,浏览器和服务端的理解也可能不一致。 - “ETag 比最后修改时间更聪明,所以不用版本号”
事实:ETag 能节省带宽,但其有效性受生成策略影响(内容哈希 vs 时间戳)。且很多 CDN 或代理会重写或丢弃 ETag。 - “长缓存就不会出问题,直接 max-age=31536000”
事实:静态资源适合长缓存,但 HTML、API 响应或不带版本号的资源则会成为更新灾难。长缓存配合不可变(immutable)+文件名指纹是安全做法;否则,部署后客户端可能永远拿不到新版本。 - “服务端不设置缓存,浏览器就不会缓存”
事实:浏览器有启发式缓存(heuristic caching),有时会基于 Last-Modified 推断一个默认 TTL。不同浏览器行为不完全一致。 - “CDN 自动帮我处理一切缓存失效”
事实:CDN 只是更靠近用户的缓存层。若你不知道 CDN 的缓存键(query string、cookie、header 是否参与),或者没有使用合适的清理/失效策略,问题同样会发生。
二、到底哪里变了?(核心趋势与事实)
- 浏览器与隐私策略正在收紧,缓存分区化/隔离变得更普遍
趋势:为防止跨站跟踪,浏览器在某些上下文中对存储(包括缓存、localStorage)做了隔离或分区。结果是某些资源在不同站点或不同隐私上下文下不会共享同一个缓存条目,导致命中率下降或行为差异。 - CDN 与边缘规则越来越复杂,默认行为并不统一
趋势:CDN 提供商加入更多边缘逻辑(边缘脚本、缓存键自定义、分层缓存、stale 策略),意味着“同一套 header 在不同 CDN 上效果不同”成为常态。 - 新的缓存指令越来越被支持(immutable、stale-while-revalidate 等)
事实:这些指令能极大改善体验(在更新时减少闪烁、保持可用性),但只有在理解各层如何支持它们后才安全使用。 - Service Worker 与客户端缓存 API 对整个缓存栈起主导作用
趋势:一旦启用了 service worker,传统的 HTTP 缓存逻辑可能被覆盖或并行执行,容易引入“明明更新了但页面还是旧的”这种问题。 - 自动化部署/构建流程使得“版本化”成为必须,但实现方式差异化明显
事实:内容哈希文件名、静态资源 CDN 推送、以及“索引文件短缓存+静态资源长缓存”成为最佳实践,但很多项目没有把这些写入 CI/CD 流程,导致人为失误频发。
三、技术要点:你应该知道的真实差别(条目式)
- Cache-Control vs s-maxage vs Surrogate-Control:
- Cache-Control 针对所有缓存节点;s-maxage 覆盖共享缓存(CDN);Surrogate-Control 是部分 CDN/代理的自定义头。若你在 CDN 前放了代理或边缘层,优先级和解析会变。
- public / private:
- public 允许共享缓存(CDN、代理)缓存;private 只允许用户的私有缓存(浏览器)。登录态页面通常要 private。
- immutable:
- 表示资源在 max-age 期间内绝对不变;适合带哈希的静态文件(例如 app.abc123.js)。
- stale-while-revalidate / stale-if-error:
- 能让客户端在后端慢或失败时使用陈旧内容,提升可用性;但要配合策略使用以免缓存永远不更新。
- ETag 与 Last-Modified:
- ETag 更精确但在分布式环境下生成策略要统一;Last-Modified 是简单回退,但精度低,可能导致不必要的 304。
- Vary:
- 告诉中间缓存按哪些请求 header 做区分(例如 Accept-Encoding、User-Agent)。误用 Vary 会导致大量重复缓存。
- Cache Key:
- 包括协议、主机、路径、查询字符串、部分 header、cookie 等。许多 CDN 可以自定义缓存键;默认行为往往不是你想的那样(例如忽略/包含 query string)。
- Service Worker Cache API:
- 独立于 HTTP 缓存;优先级和行为有时会覆盖浏览器的 normal caching。Service worker 的 scope 决定它能拦截哪些请求。
- HTML 与 index.html 的策略:
- index.html 通常不能长缓存;推荐短 TTL(或 no-cache)并使用长期缓存的静态资源带版本号。
四、实战级检查清单(排查问题/落地改正,一步到位) 1) 用 curl、浏览器 DevTools、CDN 响应头核验每一层:
- curl -I https://example.com/path 查看 Cache-Control、ETag、Age、X-Cache、CF-Cache-Status 等。
- DevTools Network 看是否 200 还是 304、是否 from memory cache、from disk cache、service worker intercept。 2) 明确缓存分层策略:
- HTML(短缓存或 no-cache + revalidation)
- 静态资源(长期 max-age + immutable + 文件指纹)
- API(按数据更新频率设短或使用 s-maxage)
3) 采用内容哈希 + 自动化构建: - 静态资源文件名带 hash,CI 自动更新引用,避免手动错误导致缓存僵尸资源。 4) 配置 CDN 的缓存键和失效策略:
- 决定是否包含 query string、是否忽略特定 cookie、使用 surrogate keys 进行批量清理。 5) 处理 Service Worker:
- 确认 service worker 的 fetch handler 是否正确处理更新场景;上线新版本时确保 service worker 可更新并刷新客户端缓存。 6) 使用并监测 stale-* 指令:
- 在非关键资源上试点 stale-while-revalidate,观察后端负载与回退效果。 7) 给 API 加版本和控制头:
- 使用 Accept 或 URL 版本化,配合合适的 Cache-Control,避免不同版本互相污染。 8) 日志与指标:
- 监控缓存命中率、Edge 命中、回源频率、Age 分布、客户端 200/304 比例,并设置报警阈值。 9) 在发布流程中加入 CDN 清理步骤或使用不需要清理的版本策略(内容哈希):
- 保证每次发布后,关键静态资产立刻可达新版本,而 HTML 或索引通过短缓存能快速失效并引用新文件。 10) 教育开发/运维团队:
- 让大家理解“请求路径、请求 header、CDN 规则”三者如何共同决定缓存的命运。
五、典型场景与解决方案(快速参考)
- 用户更新了 JS 但很多人仍看到旧页面:
- 原因:长缓存 + 未使用指纹命名 + 浏览器/CDN 缓存未失效。解决:文件指纹 + 静态资源长缓存 + index.html 短缓存或 no-cache。
- 在某些地区缓存命中率低:
- 原因:CDN 的缓存键与请求不匹配(query string、cookie 或 header 导致分片)。解决:统一 cache key、检查 CDN 日志、简化请求以增加命中。
- 频繁 304 回源导致延迟高:
- 原因:没有充分利用 Cache-Control 或 CDN 没做边缘缓存。解决:适度延长边缘缓存,利用 s-maxage,必要时用 stale-while-revalidate。
- 服务端推送(HTTP/2)与缓存冲突:
- 建议:用推送时确保客户端与代理支持并理解缓存头;否则考虑取消推送,改用链接预加载或预取。
结语:到底哪里变了?实话是:缓存的基本原理没变,但我们面对的执行环境变了——浏览器隐私隔离、CDN 边缘逻辑、Service Worker 的普及和更丰富的 HTTP 指令。这些“新因素”让过去那套凭经验随手设置 header 的方法不再稳健。解决办法不是回到过去,而是把缓存视为跨多层的合同:每一层的行为都要被明确写入你的部署、构建和监控流程里。
如果你愿意,我可以基于你现在的架构(浏览器/服务端/哪个 CDN/是否用 Service Worker)给出一份具体的缓存策略模板和部署日程表,帮你把这 91 起教训转成可执行的生产能力。想要的话,把你的当前响应头样例发给我,我们从第一层开始核对。