本节摘要:缓存让读得多的接口跑得更快、扛得更久,是 REST 可缓存约束在工程层的兑现。本节讲清
Cache-Control标记与ETag/If-None-Match条件请求的原理,并用一次"资讯接口加了缓存"的绩效演练,看命中率和新鲜度的平衡怎么拿,以及缓存失效如何防止脏数据。
阅读完本节,你应当能够:
Cache-Control 标记响应可缓存性并设置有效期。If-None-Match 条件请求如何用 304 节省流量。第 1.2 节的可缓存约束只是"要求响应明确标记可否缓存"。而到工程层,缓存做对能显著降压,做错会返回脏数据——所以要有专门的一套"加速条款"来管它。
先布一块共同直觉:缓存的本质是"把读的计算结果存起来,命中时不再重算"。它对读多写少(资讯、目录、静态配置)的接口收益最大;对强实时、强一致的写敏接口,则要小心。
服务器决定"这份响应能不能被缓存、能存多久",用 Cache-Control 头最直接:
HTTP/1.1 200 OK Cache-Control: public, max-age=3600 Content-Type: application/json {"title":"今日快讯","body":"..."}
max-age=3600 告诉客户端与中介:"这一小时内这份响应可以直接复用,别再来问服务器。"public 表示连共享缓存(CDN、代理)也可存。不同策略对照:
| 头 | 含义 | 适用 |
|---|---|---|
no-cache |
可用但用前必须校验 | 时效敏感的响应 |
no-store |
完全不可存 | 含敏感信息、支付响应的响应 |
public, max-age=N |
可被共享缓存存 N 秒 | 公开读多的资源 |
private, max-age=N |
仅客户端可存 | 含用户个性化数据的响应 |
选择是"命中率 vs 新鲜度"的权衡:max-age 越长命中越高、服务器越省,但数据新鲜度越差;短了反之。用哪个,取决于业务容忍多久的"旧"。
前面是"存 N 秒不管中间变没变",如果想"变了才重发、没变就 304",就用 ETag 条件请求:
流程:服务器给每份资源算一个 ETag(资源版本的指纹,通常基于内容哈希)。客户端带 If-None-Match 问"我这个版本还在吗",服务器比对后,若没变回 304(不重复发身体)、变了回 200 带新体与新 ETag。这样既保证新鲜又省了大量重复传输。
GET /article/1 HTTP/1.1 If-None-Match: "eTag-9a12b" HTTP/1.1 304 Not Modified
这个 304 没有响应体,客户端本地还用旧版即可——"每次请求都问,但只有真变了才下重的"。
把"谁能旧、谁必须新"的分流用一张决策图定下来:

把前面合起来做一次可度量的演练。
背景:一个热点资讯接口,高峰 QPS 冲到 5000,数据库被读打爆,平均延迟 240 毫秒。
操作:为公开的文章接口加 Cache-Control: public, max-age=60,并在网关配了 CDN 缓存,同时给文章资源带上 ETag。
结果:CDN 与客户端命中大部分热点请求,落库的只有真正变化或首次的请求,数据库 QPS 从 5000 降到约 600,平均延迟从 240 毫秒降到 40 毫秒以内。
解读:max-age=60 意味着文章最多延迟 60 秒可见新增内容,对热点资讯可接受;ETag 保证了"编辑器改完一条,改后的一秒就能拿到新版"。命中率与新鲜度在这里达到了平衡。
变式:若这是"用户余额"接口,max-age=60 会让人看到 60 秒前的余额——不可接受,必须 no-store 或短缓存 + ETag 校验,把命中率让给新鲜度。
⚠️ 常见坑:给写操作的响应也套了缓存,导致"改了没生效"。缓存只作用于 GET 这类读路径;POST/PUT/PATCH/DELETE 的响应与副作用路径要么 no-store,要么用失效机制显式清除相关资源缓存,否则就出脏数据。
缓存帮了大忙,也埋了"加速过期"的雷:存量缓存如果不失效,服务器已更新的资源在 max-age 内仍被旧值撑着。失效策略通常三选一或并用:
💡 关键直觉:缓存是把"读"的可伸缩性压到网络层。赢的是命中率,赌的是什么能容忍旧。先分清"谁能旧、谁必须新",再定 max-age 和是否上 ETag,就不会两头踩空。
读性能解决,轮到"一次干多件事还保一致"的硬骨头——批量操作与事务处理。