HTTP 缓存与 CDN 加速:缓存键设计与边缘失效
HTTP 缓存与 CDN 加速的关键是:分层时只保留一个真相 TTL,缓存键只保留影响响应体的参数并覆盖隔离维度,再用条件请求与 purge 管理新鲜度,而不是靠极短 TTL 硬扛。
- 三层缓存只保留一个真相 TTL,其余层做兜底与削峰
- 缓存键开启参数排序,仅保留真正影响响应体的参数
- s-maxage 控制边缘缓存,max-age 控制浏览器,分开配置
- 新鲜度用 ETag 条件请求加 purge,不用超短 TTL
- 含身份、会话、额度的响应一律 private, no-store
把一个接口从 800ms 优化到 60ms,最有效的手段往往不是换数据库或加机器,而是让请求根本不到源站。HTTP 缓存与 CDN(Content Delivery Network,内容分发网络) 加速解决的正是这件事。但缓存方向做错的代价同样明显:带 apikey 的响应被公共边缘节点缓存、A/B 实验流量互相串号、随机接口被缓存成“永远同一张图”。
本文不再重复“加一行 Cache-Control 就完事”的做法,而是按 缓存分层 → 缓存键 → 验证与失效 → AI 流量下的隔离 的顺序,给出一套可以直接应用到网关与 CDN(Content Delivery Network,内容分发网络) 配置上的实践,并以 IP 查询接口、随机动漫壁纸接口 为例,说明不同数据特征的接口该怎么配。
一、三层缓存:先确定谁在替你挡流量#
很多团队的缓存问题并不是“没开缓存”,而是三层缓存各写一套 TTL、互不知晓,最终拼出一个谁也没预期过的有效时长。先把职责分清楚:
| 层级 | 典型位置 | 生效指令 | 主要收益 | 典型坑 |
|---|---|---|---|---|
| 浏览器私有缓存 | 用户设备 | Cache-Control: max-age | 重复访问零请求 | 用户长期看到旧数据,且无法远程清除 |
| CDN(Content Delivery Network,内容分发网络) 边缘缓存 | 就近 PoP 节点 | s-maxage / CDN(Content Delivery Network,内容分发网络) 侧 TTL | 省回源流量与跨地域 RTT | 缓存键设计不当,导致不同用户拿到同一份数据 |
| 应用侧缓存 | Redis / 进程内内存 | 业务代码 | 降低源站计算与数据库压力 | 与 HTTP 层语义冲突,出现“双重 TTL” |
一个常见组合是:浏览器缓存 1 天、CDN(Content Delivery Network,内容分发网络) 缓存 1 小时、Redis 只存 30 秒。结果是源站数据更新后,用户一天内都看不到变化。更稳妥的做法是只保留一个“真相 TTL”,由它决定数据多久必须重新计算,其余层只做兜底与削峰,而不是各自拍脑袋定一个数字。
二、缓存键决定命中率,也决定事故范围#
CDN(Content Delivery Network,内容分发网络) 判断“两次请求是不是同一个资源”,依据是缓存键,默认通常是 scheme + host + path + 全部 query 参数。这里有两个高频问题:
- 参数顺序不同导致重复回源:
?ip=8.8.8.8&apikey=x与?apikey=x&ip=8.8.8.8在默认规则下是两个键,命中率直接减半。 - 无关参数污染:
utm_source、_t=1698...这类时间戳参数会让命中率跌到接近 0,缓存形同虚设。
处理方式是在 CDN 控制台开启 query 参数排序,并只保留真正影响响应体的参数。以 IP 查询为例,只保留 ip,把 apikey 从缓存键中移除;但前提是鉴权必须前移到边缘完成,否则任何人只要命中缓存,就能绕过额度拿到数据。
示例说明:以下代码仅用于逻辑验证,生产环境需补充异常处理、超时与重试、日志与监控告警及降级策略。
curl -sI 'https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8'观察返回头中的 cache-control、etag、age,就能判断这份响应是否可以进入共享缓存。age 大于 0 说明命中的是缓存副本而非源站。对于公共只读数据,可以这样配置:
location = /api/dongman {
add_header Cache-Control 'public, max-age=60, s-maxage=300, stale-while-revalidate=30' always;
add_header Vary 'Accept, Accept-Encoding' always;
}s-maxage 只对共享缓存生效,max-age 对浏览器生效。把两者分开,意味着“边缘缓存久、浏览器缓存短”,既能抗住热点流量,又不会让用户长期停留在旧值上。stale-while-revalidate 允许边缘在后台回源的同时继续返回旧副本,是削峰成本最低的一招。
三、验证与失效:不要用超短 TTL 强行保证新鲜#
把 max-age 设成 10 秒来“保证新鲜”,等于放弃了缓存,还把回源压力放大了数倍。更合理的组合是条件请求加精确失效:
const url = 'https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8';
let etag = null;
let cached = null;
async function getIpInfo() {
const res = await fetch(url, { headers: etag ? { 'If-None-Match': etag } : {} });
if (res.status === 304) return cached;
cached = await res.json();
etag = res.headers.get('etag');
return cached;
}304 省下的是带宽和源站计算,RTT 依然存在;跨地域访问时,边缘缓存才是主要收益来源。真正的内容更新应通过 purge 接口主动清除,而不是等 TTL 自然过期。建议给同类资源打统一的 Cache-Tag,例如 api-get、api-dongman,一次调用清空整批相关 URL。另外,stale-if-error 值得一并打开:源站返回 5xx 时,边缘继续返回旧副本,比把错误直接抛给调用方友好得多。
四、AI 流量带来的新约束:缓存的隔离边界#
今年第三季度的多起大规模网络中断再次说明,边缘节点的可用性直接决定业务可用性,把静态与只读接口尽量推到边缘,是抗故障最划算的投资。与此同时,也有推理服务因配置不匹配导致跨请求缓存页泄露的事故——这是“共享缓存层 + 会话态”混用最典型的翻车方式。落到工程上,可以记三条规则:
- 带用户身份、会话、额度或权限的中间结果,一律
Cache-Control: private, no-store; - 缓存键必须覆盖所有影响输出的维度,如租户、模型版本、语言,宁可牺牲命中率;
- POST 默认不缓存;确需缓存时使用显式 Cache-Key,并确保请求体参与键计算。
另一面,AI Agent 与自动化爬虫的抓取频率远高于人类。公开只读接口交给 CDN,配合合理的限流与 robots 声明,比在源站堆机器便宜得多。POST 类接口可参考 POST 示例接口,默认保持不可缓存。
五、不同数据特征,配不同缓存策略#
| 接口 | 数据特征 | 建议策略 |
|---|---|---|
/api/get | 结果由 IP 决定,归属地变化慢,基数高 | 缓存键只保留 ip;边缘 s-maxage=3600,浏览器 max-age=300 |
/api/dongman?type=json | 每次应返回不同结果,与“缓存同一响应”天然冲突 | 源站 no-store;若要走 CDN,改为缓存一批元数据,由边缘随机挑选 |
/doc/get 等文档页 | 内容静态,更新靠发布 | 长 TTL + 版本号或文件名做键,更新后主动 purge |
curl -sI 'https://www.api.plus/api/dongman?type=json'
curl -sI 'https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8'两条命令对比一下响应头,就能看到“可缓存”与“不可缓存”的差别,这是排查缓存是否真正生效最快的办法。
六、总结#
HTTP 缓存与 CDN 加速的核心不在指令多复杂,而在三件事:分层时只保留一个真相 TTL;缓存键既决定命中率,也决定越权与串数据的范围;用条件请求加 purge 管理新鲜度,而不是靠极短 TTL 硬扛。把这三点写进网关模板和 CDN 配置,接口的 P95 延迟和源站压力通常会有肉眼可见的变化。
在 API.PLUS 注册即可获取免费 API Key,从 IP 查询接口文档 开始实测响应头;更多接入与调优经验见 API 文章中心,若发现缓存命中率异常或响应头不符合预期,可通过 技术支持 与我们联系。