HTTP缓存机制基础:理解缓存头与协商过程

HTTP缓存,说白了就是浏览器或客户端把服务器的响应存起来,下次请求同一资源时直接拿本地副本,不用再去问服务器。这招能大幅减少延迟、省带宽,尤其对移动端来说,少发一次请求可能就是几百毫秒的差距。

我个人习惯把HTTP缓存分成两个阶段:强缓存和协商缓存。理解这两者的区别,是配置OkHttp缓存的第一步。

核心观点:强缓存命中时,客户端根本不发请求。协商缓存命中时,客户端发请求但服务器只返回304状态码(无Body)。前者比后者快得多。

2.1 强缓存——我说了算

强缓存由服务器通过响应头告诉客户端:这个资源在某个时间点之前都是新鲜的,你直接用吧。

控制强缓存有两个主要的响应头:Expires 和 Cache-Control。

响应头 格式 说明 优先级
Expires HTTP日期(如 Wed, 21 Oct 2025 07:28:00 GMT) 指定过期时间。客户端与服务器时钟可能不同步,易出问题。 低(被Cache-Control覆盖)
Cache-Control: max-age=3600 相对秒数 指定从响应生成时刻起多少秒内新鲜。更灵活,更推荐。 高

嗯,这里要注意:Expires 是HTTP/1.0时代的产物,现在基本被 Cache-Control 取代。但很多老系统还在用,所以OkHttp对两者都支持。我在项目中遇到过一种情况——服务器返回了 Expires 但时间比本地时钟早,结果缓存根本不起作用。后来改成 Cache-Control: max-age 就稳定多了。

个人经验:只要后端能控制,一律用 Cache-Control。如果必须兼容老客户端,可以两个一起发,但 Cache-Control 优先级更高,浏览器会自动选择。

2.2 协商缓存——你说了算,但得问问我

强缓存过期了怎么办?客户端会带着缓存资源的“指纹”去问服务器:“这个资源变没变?没变就给我304,我继续用旧的;变了你就把新的扔过来。”

这个“指纹”有两种常见机制:Last-Modified / If-Modified-Since 和 ETag / If-None-Match。

  • Last-Modified(响应头):服务器返回资源最后修改时间。客户端下次请求时带上 If-Modified-Since: 那个时间。服务器比对时间,没变就返回304。
  • ETag(响应头):服务器返回资源的内容哈希或版本标识。客户端下次请求时带上 If-None-Match: 那个ETag值。服务器比较ETag,没变就返回304。

我个人更偏爱 ETag。为什么?因为 Last-Modified 只能精确到秒,如果同一秒内修改了多次,它就没法区分。而且某些动态生成的内容根本就没有“最后修改时间”的概念。我曾经踩过一个坑:CDN上缓存了一张图片,但回源文件实际上已经变了,只是因为修改时间没变,协商缓存一直返回304,用户看到了旧图。后来加上ETag就解决了。

避坑指南:ETag 比 Last-Modified 更精确,但计算ETag会消耗服务器CPU。如果资源非常大,每次请求都算哈希可能扛不住。我的建议是:静态资源用强哈希(比如MD5),动态资源用版本号或增量标识。

2.3 缓存协商的完整流程

把上面两个阶段串起来,就是一次HTTP请求的缓存完整流程。我习惯用下面这张图来给团队讲清楚。

HTTP缓存协商流程 客户端发起请求 检查本地是否有缓存? (强缓存阶段) 缓存未过期 直接使用本地缓存(强缓存命中) 缓存过期 发起条件请求(协商阶段) 服务器判断资源是否变化 (比对 ETag 或 Last-Modified) 未变化 → 返回 304 已变化 → 返回 200 + 新资源 继续使用本地缓存 更新本地缓存 上图为简化流程,实际OkHttp通过 Interceptor拦截器链管理缓存决策。

2.4 OkHttp 中与缓存相关的常见头部

除了服务端返回的 Cache-Control 和 ETag,OkHttp 客户端请求时也可以主动控制缓存行为。下面是我经常用到的几个指令。

请求头 / 指令 作用 场景示例
Cache-Control: no-cache 强制每次请求都走协商缓存(不直接用强缓存) 用户刷新按钮
Cache-Control: no-store 完全禁止缓存(连协商都不做) 支付、登录等敏感接口
Cache-Control: max-stale=5 客户端愿意接受过期不超过5秒的缓存 弱网环境,优先展示旧数据
Cache-Control: min-fresh=10 客户端要求缓存剩余新鲜度≥10秒 实时性要求较高的列表

小技巧:在 OkHttp 的 Cache 对象中,你可以通过 CacheStrategy 自定义缓存策略。比如强制走网络、强制走缓存、或者根据时间折中。这些我后面会详细讲。

2.5 实际项目中我踩过的坑

我曾经维护过一个资讯 App,列表接口加了 Cache-Control: max-age=60,但用户频繁下拉刷新,每次刷新都请求新数据,缓存根本没用上。后来发现是前端在请求头里加了 Cache-Control: no-cache——自己把自己的缓存给禁了。你看,前后端对缓存的认知不一致就容易出问题。

另一个坑是:OkHttp 默认的 Cache 只缓存 GET 请求的响应。如果你有个 POST 请求也想缓存(比如搜索),需要自己写 Interceptor 处理。这一点很多新手不知道。

一句话记住:强缓存决定“省不省请求”,协商缓存决定“省不省带宽”。配置好这两者,你的 App 就能在性能和实时性之间找到平衡。