如何为 API 响应数据设计合理的 HTTP 缓存策略(ETag、Last-Modified)?
解读
面试官问的不是“要不要缓存”,而是“怎么让缓存既快又准”。国内网络环境复杂(弱网、跨省 CDN、运营商劫持),业务场景多样(秒杀、 feeds、配置下发),还要兼顾合规(个人信息不过期、审计可追踪)。回答时要体现:
- 知道 ETag/Last-Modified 的语义差异与组合方式;
- 能在 Android 侧用 OkHttp 完整实现“无修改则 304”;
- 能根据业务 TTL、用户账号、数据敏感度做分级策略;
- 异常场景(CDN 丢 304、运营商改头、系统时间错乱)有兜底。
知识点
- HTTP/1.1 缓存语义:强缓存(Cache-Control max-age)与协商缓存(ETag/Last-Modified)优先级;
- ETag 生成算法:文件型用 SHA-256/MD5,DB 型用版本号或组合字段哈希,分布式场景要消除机器差异;
- Last-Modified 精度仅到秒,适合低频更新的大文件;ETag 可识别“秒级内多次变更”;
- OkHttp 默认只认识 Cache-Control,要让 304 生效必须给请求加上
If-None-Match/If-Modified-Since; - 国内 ROM 可能关闭系统时钟同步,导致 Last-Modified 对比失效,需双字段并存;
- 合规要求:含用户隐私的接口(如个人资料)必须
private, no-store或走 HTTPS+短 TTL; - 折叠屏/多窗口场景,Activity 重建时缓存命中可省一次流量,配合 Jetpack Lifecycle 自动注销回调;
- 弱网优化:在 304 回包丢失时,本地给 200 响应补一个“伪 304”标记,触发 Repository 层直接拿 DB,避免 UI 空白。
答案
“我在项目中把缓存拆成三层:CDN 层、网关层、客户端层。Android 侧聚焦客户端,策略如下:
-
接口分级
配置类(如首页栏目)TTL=10 分钟,允许 CDN 缓存;用户类(如订单列表)TTL=0,走 private 协商缓存。 -
ETag 生成
后端对返回 JSON 做稳定哈希(字段排序后再 SHA-256),把哈希前 8 位作为 ETag,既省流量又避免冲突。Last-Modified 取 update_time 字段,秒级精度。 -
OkHttp 配置
新建 Cache 目录 50 MB,在拦截器里统一追加
.addInterceptor(chain -> { Request req = chain.request().newBuilder() .removeHeader("If-None-Match") .addHeader("If-None-Match", CacheHelper.getETag(req)) .build(); return chain.proceed(req); })
这样 304 回包会自动落到 Cache 层,上层 Repository 直接拿 Response.cacheResponse() 判断即可。 -
异常兜底
若 CDN 错误返回 200 且 body 未变,我用本地 CRC32 与上一次保存值比对,一致则抛“伪 304”事件,走 DB 缓存;时间错乱时忽略 Last-Modified,只用 ETag。 -
合规与灰度
含手机号、定位的接口强制Cache-Control: private, max-age=0, must-revalidate,并在用户登出时调用Cache.evictAll()防止数据残留。灰度阶段给 5% 用户关闭缓存开关,通过 Firebase RemoteConfig 实时下发。
上线后,首页接口流量下降 42%,弱网首帧平均提前 280 ms,用户隐私 0 泄漏审计问题。”
拓展思考
-
如果业务需要“秒级推送”怎么办?
可在 ETag 外再引入“时间窗口版本号”:后端每 10 秒累加一次版本,客户端长连接收到推送后把版本号写入 SharedPreferences,下次请求带If-None-Match: "v123",实现“伪实时” yet 仍走缓存。 -
多端一致性如何保障?
同一用户在手机、车机、TV 三端登录,ETag 算法必须包含 userId+deviceType,否则车机拿到手机缓存会字段缺失。可以用 GraphQL 的@cacheKey指令把变量拼进哈希。 -
未来 HTTP/3 0-RTT 与 ETag 的结合点?
QUIC 0-RTT 早期数据不能携带条件请求头,需在 TLS 层外再签一个“早期 ETag”,服务端用此字段做试探性缓存命中,降低冷启延迟,Android 侧可封装为QuicCacheInterceptor提前预热。