HTTP 缓存与 CDN 加速:API 场景缓存实践
API 缓存不能只加 max-age:需区分浏览器私有缓存与 CDN 共享缓存,正确使用 Cache-Control、ETag 与条件请求,按接口类型分级,并通过缓存键设计和回源保护让源站在突发流量下仍可控。
- 区分 public、private、no-store,带鉴权的响应不进共享缓存
- ETag 基于内容版本生成,条件请求才能稳定返回 304
- 缓存键只保留影响响应的参数,剔除跟踪与噪声参数
- 随机类接口把随机性前移到带内容哈希的 URL 再长缓存
- 用 proxy_cache_lock 与 stale-if-error 保护回源
引言:API 也要认真做缓存#
很多团队把缓存当成静态资源的事:图片、JS、CSS 配长 TTL,接口则默认回源。但在 2026 年,API 面对的流量结构已经变了:除了真实用户,还有大量自动化 agent 在短时间内重复调用只读接口。这类流量突发、幂等、重试频繁,如果全部打到源站,数据库和限流层会先扛不住。GitHub 近期把海量仓库对象迁到对象存储、让读请求独立扩展的思路,对 API 同样有启发:把不可变或弱变化的内容放到边缘,让源站专注处理缓存未命中和写请求。
本文不讨论“加个 CDN(Content Delivery Network,内容分发网络) 就快了”这种结论,而是从 HTTP 缓存语义、CDN 缓存键设计、接口分级策略和回源保护四个方面,给出一套能直接落地的做法。
一、先把 HTTP 缓存语义用对#
缓存不是加一个 max-age 就完事。首先要区分浏览器私有缓存和 CDN(Content Delivery Network,内容分发网络) 共享缓存,再决定哪些响应可以被谁缓存、缓存多久、失效时怎么办。
| 指令 | 作用 | 常见误用 |
|---|---|---|
max-age | 浏览器私有缓存有效期 | 给用户相关数据配长 TTL |
s-maxage | CDN(Content Delivery Network,内容分发网络) 等共享缓存有效期,优先级高于 max-age | 只写 max-age,导致边缘不缓存 |
public / private | 是否允许共享缓存存储 | 带鉴权响应未标 private |
no-cache | 可以存,但每次使用前必须校验 | 误以为等于不缓存 |
no-store | 完全不存储 | 写接口漏配,被中间层缓存 |
stale-while-revalidate | 过期后先返回旧值,同时后台刷新 | 没有配后台更新,旧值一直不刷新 |
stale-if-error | 源站错误时继续使用旧副本 | 故障时直接把 5xx 抛给调用方 |
一个比较稳妥的只读接口响应头如下:
示例说明:以下代码仅用于逻辑验证,生产环境需补充异常处理、超时与重试、日志与监控告警及降级策略。
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=600, stale-if-error=86400
ETag: "ip-v3-8f3c2a"
Vary: Accept-Encoding
Age: 42
ETag 应基于内容版本生成,而不是进程启动时间或随机数。客户端带上 If-None-Match 后,服务端返回 304 即可,响应体为空,能显著降低带宽和序列化开销。
1.1 Vary 与缓存键的边界
Vary: Accept-Encoding 是常见且安全的。但不要轻易用 Vary: Authorization 或 Vary: X-Forwarded-For 来做共享缓存:前者会让缓存碎片化,后者会为每个客户端生成一份副本,命中率趋近于零,还可能造成越权读取。带鉴权的响应默认应使用 private 或 no-store。
1.2 用条件请求验证缓存行为
curl -sI https://www.api.plus/api/get?apikey=YOUR_KEY&ip=1.1.1.1
curl -s -H 'If-None-Match: "ip-v3-8f3c2a"' -o /dev/null -w '%{http_code}\n' https://www.api.plus/api/get?apikey=YOUR_KEY&ip=1.1.1.1
第一次看响应头里的 Cache-Control、ETag、Age,第二次期望拿到 304。如果没有 304,先检查反向代理是否剥离了条件请求头。
二、CDN(Content Delivery Network,内容分发网络) 分层的缓存键与回源设计#
生产环境通常不是“边缘直接回源”这么简单,而是边缘节点、中间盾层、源站三层。边缘负责吸收重复读,盾层负责把多个边缘的回源合并成少量请求,源站只处理真正未命中的请求。这里的关键不是把边缘命中率刷到多高,而是让回源 QPS(Queries Per Second,每秒请求数) 可控。
缓存键只应包含影响响应内容的参数。跟踪参数、时间戳、无意义排序都应被规范化掉;多租户接口要把租户维度纳入缓存键,但不要直接放 Authorization 原文,可以用哈希后的租户 ID。
proxy_cache_path /var/cache/nginx/api levels=1:2 keys_zone=api_cache:200m
max_size=20g inactive=2h use_temp_path=off;
proxy_cache_key "$scheme$request_method$host$uri$is_args$args";
proxy_cache_valid 200 5m;
proxy_cache_valid 404 30s;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status always;
proxy_cache_lock 可以把同一时刻对同一 key 的并发回源合并成一次;proxy_cache_background_update 配合 stale-while-revalidate 实现后台刷新;use_stale 则在源站抖动时提供兜底。上线后重点看 X-Cache-Status 的 HIT、MISS、UPDATING 比例,以及按接口维度统计的回源 QPS(Queries Per Second,每秒请求数)。
三、按接口类型制定缓存策略#
同样是 GET,不同接口的缓存语义可能完全相反。下面结合 API.PLUS 的几类接口说明。
3.1 查询类接口:IP 查询
IP 查询接口的结果取决于参数中的 IP,而不是请求者的 IP。如果响应只与查询参数有关,可以把参数规范化后作为缓存键,设置较长的 s-maxage,让边缘承担绝大部分重复查询。但如果结果与请求者所在位置、账号权限有关,就必须使用 private 或 no-store,不能共用边缘缓存。
Cache-Control: public, s-maxage=3600, stale-while-revalidate=600
Vary: Accept-Encoding
还要注意参数顺序和大小写。缓存键里最好只保留白名单参数,避免 ?ip=1.1.1.1&utm_source=x 和 ?ip=1.1.1.1 被当成两个 key。
3.2 随机类接口:随机动漫壁纸
随机动漫壁纸接口每次请求都返回不同的图片。如果直接给 CDN(Content Delivery Network,内容分发网络) 长缓存,随机语义会退化成一张固定图片。两种可行做法:一是返回 no-store,由客户端每次回源;二是把随机性前移到 URL——接口返回带内容哈希的图片地址,再让 CDN(Content Delivery Network,内容分发网络) 只对最终图片地址做长缓存。第二种更推荐,既保留随机体验,又让图片可以 immutable 缓存。本站随机壁纸接口支持 type=json 直接返回图片地址:
GET https://www.api.plus/api/dongman?type=json
{
"url": "https://www.api.plus/api/dongman/desktop/004c2cee87467e76f25ec7db654d5e12.jpg",
"size": "1.16 MB",
"title": "004c2cee87467e76f25ec7db654d5e12",
"category": "desktop"
}
拿到这个带内容哈希的图片地址后,再对它设置长缓存:
Cache-Control: public, max-age=31536000, immutable
ETag: "004c2cee87467e76f25ec7db654d5e12"
3.3 写接口:POST 示例
POST 默认不可缓存。POST 示例接口这类请求应显式设置 no-store,避免中间层误缓存。如果希望结果可复用,可以让服务端返回 303 重定向到一个可缓存的 GET 资源。幂等键适合放在请求体或 Idempotency-Key 头中,不要塞进 URL 缓存键。
四、agent 规模流量下的回源保护#
自动化 agent 的调用模式和人类不同:它们会在短时间内重复请求同一资源,失败后立即重试,而且往往只读。缓存是第一道防线,但还需要配合限流和退避,否则缓存击穿时源站会被瞬间打满。
- 请求合并:用
proxy_cache_lock或 CDN(Content Delivery Network,内容分发网络) 的 request collapsing,避免热点 key 并发回源。 - 短 TTL 加
stale-if-error:源站故障时继续提供旧数据,控制故障半径。 - 明确退避:对 agent 返回 429 和
Retry-After,减少无效重试。 - 分层缓存:边缘短 TTL、盾层长 TTL,源站只做校验和写操作。
- 可观测性:持续跟踪命中率、回源 QPS(Queries Per Second,每秒请求数)、P95 延迟和按接口的缓存状态分布。
安全边界同样重要。共享缓存配置错误可能导致跨请求读到他人的数据,这类问题和模型侧 KV cache 跨请求暴露的本质一样:缓存键没有包含完整的隔离维度。上线前做一次“换账号请求同一 URL”的验证,确认鉴权响应不会被共享缓存命中。
五、上线检查清单#
- 每个 GET 接口明确标注
public、private或no-store,不依赖默认值。 - 写接口返回
no-store;需要复用时用 303 加可缓存 GET。 ETag基于内容版本生成,条件请求能稳定返回 304。- 缓存键只包含影响响应的参数,去掉跟踪参数和噪声参数。
- 验证
Vary不会造成缓存碎片化或越权。 - 配置
stale-while-revalidate与stale-if-error,并做回源压测。 - 监控命中率与回源 QPS(Queries Per Second,每秒请求数),给源站设置回源限流和熔断。
总结#
HTTP 缓存与 CDN(Content Delivery Network,内容分发网络) 加速的核心不是“多缓存一会儿”,而是把响应语义、缓存键、失效策略和回源保护设计成一个整体:让可共享的响应尽量留在边缘,让私有数据明确不可缓存,让源站在故障和突发流量下仍能保持可控。更多接口调用与工程实践可以在API 文章中心查看,遇到缓存与鉴权组合问题可通过技术支持交流。你也可以在 API.PLUS 注册并获取免费 API Key,用IP 查询接口、随机动漫壁纸接口和POST 示例接口实际验证本文的缓存策略。