HTTP 缓存与 CDN 加速实践:缓存键、校验器与不该进缓存的响应
面向开发者的落点是三步:先判断响应允许被哪类缓存存储,再显式定义缓存键,最后决定校验器与失效路径。命中率问题多数出在缓存键与 Vary,风险最大的问题则是把带用户数据的响应放进共享缓存;具体 CDN 默认行为与指标口径需按厂商文档核对。
这篇只谈三个能直接落到代码和响应头上的点:缓存键怎么定、校验器怎么给、哪些响应根本不该进共享缓存。文中不给命中率、延迟或 QPS 数字——本次没有可核验的测试记录,任何百分比都只是猜测。CDN 厂商的默认行为、失效接口限流与指标口径,需按各自文档核对,本文不做断言。
一、先分清由谁存储、由谁复用#
浏览器里的是私有缓存,CDN 边缘节点上的是共享缓存,一份响应可能被许多互不相关的请求复用。因此判断顺序是:这个响应能不能被共享缓存存储,其次才是存多久。
no-store:不允许存储,适合含敏感字段的响应。no-cache:允许存储,但每次复用前必须校验。它不等于“不缓存”,这是排查中最常见的误读。private/public:分别指向私有缓存与共享缓存。s-maxage:面向共享缓存的新鲜期,与max-age分开表达。
以上语义由 HTTP 缓存标准(RFC 9111,取代早期 RFC 7234)定义;本文未逐条核对其条款编号与措辞,落地时请以标准原文与所用 CDN 的文档为准。
二、缓存键:命中率问题多半出在这里#
边缘节点通常以 scheme、host、path 和查询串为基础构造键。三类常见偏差:
- 无关参数混入:
utm_*、时间戳、随机数这类参数一变,键就变,同一份内容被切成许多副本。 - 参数顺序不稳定:
?a=1&b=2与?b=2&a=1在未规范化时可能是两个键。 - Vary 设置失衡:响应随请求头变化(如语言、编码)时需要用
Vary声明;但Vary: *事实上让响应几乎无法被复用,而漏写则会让他人拿到不适配的版本。
实践上建议把业务真正需要的参数写成显式白名单,而不是“默认全保留再删几个”。
示意片段(未执行)
下面仅说明“白名单 + 排序”的结构,未实际运行,example.com 为占位域名。
// 示意代码,未执行;仅用于说明缓存键的构造方式
function cacheKey(req) {
const url = new URL(req.url, 'https://example.com');
const keep = ['page', 'lang']; // 业务相关参数白名单
const query = keep
.filter((k) => url.searchParams.has(k))
.sort()
.map((k) => `${k}=${url.searchParams.get(k)}`)
.join('&');
return `${req.method} ${url.pathname}?${query}`;
}
三、校验器:ETag 与 Last-Modified 的取舍#
新鲜期过后,缓存可以带着校验器回源询问“还是这份吗”,未变则回 304 而不重传正文。ETag 比时间戳更精确,但要注意两点:
- 弱校验器与强校验器语义不同,压缩、转码链路是否影响一致性要先确认。
- 源站多实例时,同一资源的 ETag 必须一致。否则边缘拿着旧校验器去问新实例,会持续拿到
200,校验通道等于失效。
另外,304 仍然是一次往返,它省的是带宽和序列化,不是网络延迟。
四、不该进共享缓存的响应#
最需要警惕的不是命中率低,而是把带用户数据的响应留在了边缘:
- 带
Authorization的请求,以及设置了Set-Cookie的响应,默认不应被共享缓存存储。 - 按登录态裁剪字段的 JSON,例如多出
email、余额或权限位。 - 错误响应也要区分:临时的 5xx 被缓存后,故障会在边缘被放大并延长。
一旦发生,影响范围是所有命中该键的请求者,而不只是触发写入的那个人。
五、上线前的核对清单#
- 为每个端点标注“可被共享缓存 / 仅私有 / 不存储”,并写下依据。
- 列出缓存键的显式组成,确认无关参数已被剥离。
- 确认校验器的生成方式,以及多实例下是否一致。
- 明确发布时的失效路径与粒度:单 URL、标签还是整站。
- 把监控里的命中率口径与厂商文档对齐——HIT、MISS、BYPASS、STALE 的统计方式各家不同,具体定义待核实。
边界说明#
本文不涉及具体 CDN 的配置语法、默认 TTL、失效接口配额与费用,这些属于厂商文档范畴,本次没有可引用的材料,不做推测。若要在真实环境验证,需要可核验的测试记录:固定响应头、固定缓存键、记录回源次数与状态码分布,并写明环境与方法。