先记住这个答案
可以。当响应未携带 Expires 或 Cache-Control: max-age 时,若状态码允许启发式缓存,HTTP 缓存会用 Last-Modified 与 Date 的差值乘以约 10% 作为新鲜期。例如 Last-Modified 距今十天,则新鲜期约为一天。此估算依赖修改时间与内容变更强相关,风险是低频更新或动态内容会在估算期内被误当作新鲜,需靠条件请求或主动配置显式新鲜度来规避。
- 启发式缓存是最后兜底,应避免依赖
- 新鲜期约为 Last-Modified 到 Date 的 10%
- 警惕启发式缓存导致过期内容继续服务
启发式新鲜期的计算与判定条件
当响应没有 Expires 也没有 Cache-Control 的 max-age 指令时,缓存仍可能存储并复用响应,前提是状态码被定义为可启发式缓存,例如 200、301、410 等。RFC 9111 第 4.2.2 节指出,若响应带有 Last-Modified,缓存可据此估算新鲜度:通常取 Date 与 Last-Modified 差值的一定比例,常见实现是 10%。这个比例并未硬编码为标准值,只要求是合理上限。
具体算法为:freshness_lifetime = (Date - Last-Modified) × 0.1。若响应没有 Last-Modified,则没有可用于启发式计算的基准时间,缓存通常会认为响应已过期并在下次请求时向源站重新验证。
APT 软件源频繁发布更新的失败场景
一个企业内网镜像服务器未配置 Cache-Control,偶尔通过 Last-Modified 透露软件包的修改时间。某软件包在一个月前被构建,Date 与 Last-Modified 相差约 30 天,于是缓存按 10% 估算出 3 天新鲜期。上午 10 点源站发布了安全补丁,但该包在缓存中仍被视为新鲜,最长 3 天内不会回源验证,客户端持续得到含有漏洞的旧包。
修复方式是停止依赖启发式缓存:源站为这类动态资源显式设置 Cache-Control: no-cache 或 max-age=0,并配合 ETag 做条件请求。若仍需短缓存,可设 max-age=60 并永远使用 Last-Modified 或 ETag 验证,使每次回源都能 304 且网络开销可控。这一改动避免发布后被动等待 TTL 自然过期。
启发式缓存的失效边界与运维代价
启发式缓存只在完全没有显式新鲜度信息时才起作用,一旦存在 Expires 或 Cache-Control,就会优先采用显式值。所以风险集中于配置缺失的旧服务器或第三方接口。另一个前提是状态码默认允许,如 404 响应也可被启发式缓存,但若 404 后来变为 200,在新鲜期内用户仍会看到页面丢失。
处理方法是审计没有缓存头的响应,为其补上合适的 Cache-Control。若无法改动源站,可在代理层规范化:添加 Cache-Control: no-cache 或统一 max-age 策略。代价是需要持续监控响应头变化,避免新路径再次漏配。开发者应把 Last-Modified 仅用作验证器而非新鲜度依据,真正可靠的是配套 ETag 或显式时间。
容易答错的地方
- Last-Modified 可精确反映内容变化
- Last-Modified 精度只有秒级,同一秒内多次修改可能漏判;且服务器可自由设置该值,不保证与内容一致。它只适合弱验证,不能作为内容版本号。正确做法是优先使用 ETag 或显式新鲜度控制,Last-Modified 仅作回退。
- 没有缓存头就一定不会缓存
- 很多响应没有 Cache-Control 和 Expires,但浏览器仍会按启发式规则缓存,如那些 200 响应。若不想让浏览器缓存,应当显式发送 Cache-Control: no-cache 或 no-store,不能指望缺少响应头来防止存储。
面试官还会怎么问?
启发式缓存中 10% 比例是标准规定吗?
不是。RFC 9111 只要求启发式新鲜度应选用不会显著增加误用的比例,并没有给出具体的 10% 示例;10% 是 Chrome 等常见实现的选择,其他浏览器或中间缓存可能调整。
响应带有 Vary 头且未命中时,启发式新鲜度如何计算?
Vary 只影响缓存键,不影响新鲜度。未命中表示没有存储的表示,需向源站发完整请求;新响应若无显式新鲜度指令,仍可走启发式流程,其 Last-Modified 照常参与计算。
如果 Last-Modified 晚于 Date(时钟不一致)会怎样?
差值将变为负数,实现会将其视为零新鲜期,即缓存立即过期,每次请求都会回源验证。实际收益低,但不会错误延长缓存,可促使开发者修复时间偏差。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。