2. 缓存控制头深度解析:Cache‑Control 指令集、max‑age/s‑maxage/必须重新验证/stale‑while‑revalidate、Expires 与 ETag
2.1 Cache‑Control 指令集概述
HTTP/1.1 Cache‑Control 头是客户端和服务器之间协商缓存行为的主要手段。下表列出核心指令及其主要用途:
| 指令 | 方向 | 含义 | 示例 |
|---|---|---|---|
max‑age=delta | 请求 / 响应 | 资源被视为新鲜的最大秒数 | max‑age=3600 |
s‑maxage=delta | 响应 | 共享缓存(如 CDN)专用最大新鲜时间,覆盖 max‑age | s‑maxage=86400 |
must‑revalidate | 响应 | 一旦过期,必须向源服务器重新验证,不可使用陈旧响应 | must‑revalidate |
stale‑while‑revalidate | 响应 | 在指定秒数内允许使用陈旧响应,同时后台异步重新验证 | stale‑while‑revalidate=86400 |
no‑cache | 请求 / 响应 | 每次使用前必须向源服务器验证(但可存储) | no‑cache |
no‑store | 请求 / 响应 | 禁止任何缓存存储 | no‑store |
private | 响应 | 仅允许私有缓存(浏览器)存储 | private |
public | 响应 | 允许任何缓存(包括 CDN)存储 | public |
经验:Retrofit + OkHttp 默认缓存由
Cache‑Control 驱动。服务端未返回此头时,OkHttp 会回退到 Expires 和 Last‑Modified 启发式算法。2.2 max‑age 与 s‑maxage
max‑age 是客户端或服务器声明的新鲜度基准:从响应生成时刻起,经过 delta 秒内可直接使用存储副本。s‑maxage 仅对共享缓存(如代理、CDN)生效,且优先级高于 max‑age。当同时出现时,非共享缓存(浏览器)忽略 s‑maxage。
设置示例(OkHttp 拦截器中添加缓存控制头):
// 在 OkHttp 网络拦截器中加入 Cache-Control 头
Interceptor networkInterceptor = chain -> {
Request original = chain.request();
Request.Builder requestBuilder = original.newBuilder()
.header("Cache-Control", "max-age=60, s-maxage=300");
Response response = chain.proceed(requestBuilder.build());
// 也可在响应侧强控
Response.Builder responseBuilder = response.newBuilder()
.header("Cache-Control", "public, max-age=300, must-revalidate");
return responseBuilder.build();
};
2.3 must‑revalidate 与 stale‑while‑revalidate
- must‑revalidate:当资源过期后,缓存必须向源服务器发起条件验证(如
If‑None‑Match/If‑Modified‑Since),不允许返回陈旧响应。适用于对数据一致性要求极高的接口(如支付状态)。 - stale‑while‑revalidate:允许在指定秒数内“临时”返回过期数据,同时后台悄无声息地触发重新验证。该指令能极大改善感知延迟,适合新闻、排行榜等可容忍短暂延迟的场景。
| 指令 | 过期后行为 | 适用场景 |
|---|---|---|
must‑revalidate | 阻塞等待验证结果 | 订单、库存、用户资料 |
stale‑while‑revalidate | 立即返回陈旧数据,异步验证 | 首页、文章列表、动态配置 |
服务端响应头配置示例:
# 服务端返回头(PHP / Nginx 均可)
HTTP/1.1 200 OK
Cache-Control: public, max-age=300, must-revalidate
# 或者使用 stale-while-revalidate
Cache-Control: public, max-age=300, stale-while-revalidate=86400
注意:若同时设置
must‑revalidate 和 stale‑while‑revalidate,must‑revalidate 优先(即过期后必须等待验证,不会返回陈旧数据)。两者通常是互斥的。2.4 Expires 与 ETag
Expires 是 HTTP/1.0 的原始过期字段,指定一个 GMT 时间。若同时存在 Cache‑Control: max‑age,则 max‑age 覆盖 Expires(因为 Cache‑Control 优先级更高)。
ETag 提供一种强验证器:服务器为资源生成唯一标识(通常基于内容哈希)。客户端在后续请求中通过 If‑None‑Match 携带上次的 ETag,若资源未变则返回 304 Not Modified,不传输正文。配合 Cache‑Control 可实现“过期→条件验证→304 直接使用缓存”的流程。
典型条件验证过程(OkHttp 自动处理):
// 第一次请求响应
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
ETag: "abc123"
// 3600 秒后客户端发起条件请求
GET /data HTTP/1.1
If-None-Match: "abc123"
// 服务器返回 304,无 Body
HTTP/1.1 304 Not Modified
2.5 完整流程图:缓存决策路径
下图展示从客户端请求到缓存命中的完整决策链,突出 max‑age、s‑maxage、must‑revalidate 及 ETag 的交互:
2.6 调试与常见错误
调试第一步:在 OkHttp 的
EventListener 或日志拦截器中打印 Cache‑Control、Expires、ETag 和 Age 头,确认服务器是否按预期返回。// OkHttp 日志拦截器示例(使用 HttpLoggingInterceptor)
HttpLoggingInterceptor logging = new HttpLoggingInterceptor(message -> {
// 过滤出缓存相关头
if (message.startsWith("Cache-Control") || message.startsWith("ETag")
|| message.startsWith("Expires") || message.startsWith("Age")) {
Log.d("CacheHeaders", message);
}
});
logging.setLevel(HttpLoggingInterceptor.Level.HEADERS);
常见错误列表:
- s‑maxage 不生效:确认缓存是否标记为
public,且请求路径经过代理/CDN。 - ETag 未携带:OkHttp 默认会在缓存过期后自动添加
If‑None‑Match,但若拦截器清除了请求头,ETag会丢失。 - must‑revalidate 导致请求阻塞:在弱网环境下,须在客户端设置超时(见上文 Netty/OkHttp 超时配置)。
- Expires 与 max‑age 冲突:同时存在时
max‑age胜出,但有些老旧 CDN 只认Expires,务必双写。
2.7 性能与稳定性优化
| 优化点 | 说明 |
|---|---|
合并使用 max‑age + stale‑while‑revalidate | 大幅降低首帧延迟,同时保证数据最终一致 |
为不同 API 设计差异化的 max‑age | 用户信息用 60s,产品列表用 600s,静态资源用 86400s |
| 服务端生成强 ETag(内容哈希) | 避免弱 ETag(如 W/"...")导致的无效验证 |
共享缓存(CDN)启用 s‑maxage | 让 CDN 缓存更久,用户层 max‑age 可设较短以防内容过旧 |