5.1 缓存层次:从浏览器到服务器 本节摘要:缓存是性能优化中杠杆率最高的一招——最好的请求是被免掉的那个。本节画出从浏览器到后端的四层缓存地图:浏览器私有缓存、共享代理缓存、httpd 的 modcache 磁盘缓存、应用层缓存,讲清新鲜度过期与协商校验两种机制、Cache-Control 头的配置方法、modcache 的启用与典型坑,最后给一套"静态资源长缓存、动态内容协商缓存"的完整策略。 先明确学习目标 阅读完本节,你应当能够: 画出四层缓存地图并说明每层存什么、谁判定; 区分强缓存与协商缓存及其状态码表现(200 from cache 与 304); 用 Cache-Control 与 Expires 配置静态资源的缓存策略;
本节摘要:缓存是性能优化中杠杆率最高的一招——最好的请求是被免掉的那个。本节画出从浏览器到后端的四层缓存地图:浏览器私有缓存、共享代理缓存、httpd 的 mod_cache 磁盘缓存、应用层缓存,讲清新鲜度过期与协商校验两种机制、Cache-Control 头的配置方法、mod_cache 的启用与典型坑,最后给一套"静态资源长缓存、动态内容协商缓存"的完整策略。
阅读完本节,你应当能够:
说到"给网站加缓存",新手想到的往往是装一个模块。实际上从用户到数据之间天然存在四层缓存位,每层的存法与判定者都不同:
| 层 | 存在哪 | 存什么 | 谁判定可否使用 |
|---|---|---|---|
| 浏览器私有缓存 | 用户设备 | 页面资源 | 浏览器按响应头 |
| 共享代理/CDN 缓存 | 网络中间层 | 可共享的响应 | 中间层按响应头 |
| httpd 服务器缓存 | 服务器磁盘/内存 | 后端与生成结果 | httpd 按配置与响应头 |
| 应用缓存 | 应用进程/外部存储 | 数据与计算结果 | 应用代码 |
设计缓存策略时的思考顺序应当从外往内:能停在浏览器里的请求连服务器都不用到,这是最便宜的;停不到的看能不能停在 CDN;再停不到才落到服务器缓存;最后才是应用缓存。多数性能事故的根因是顺序反了——服务器缓存做得精致,响应头却写得让浏览器每次都来问。

HTTP 缓存的一切围绕两种机制运转。
新鲜度(强缓存):响应头直接声明"这份内容在某时刻前是新鲜的"。新鲜期内,浏览器连请求都不发。声明靠 Cache-Control 的 max-age:
<IfModule mod_expires.c> ExpiresActive On # 图片与字体:一年,"永不担心过期"的底气来自下一节的指纹方案 ExpiresByType image/png "access plus 1 year" ExpiresByType font/woff2 "access plus 1 year" # css/js:一个月 ExpiresByType text/css "access plus 1 month" ExpiresByType application/javascript "access plus 1 month" # html:不加长缓存,它是引用其他资源的入口,必须保持新鲜 ExpiresByType text/html "access plus 0 seconds" </IfModule>
协商(条件请求):新鲜期过了,浏览器带着上次响应留下的验证凭证(Last-Modified 时间戳或 ETag 内容指纹)来问"变了吗"。没变,服务器只回一个 304,不带正文;变了,回全新 200。协商的代价是一个完整往返,所以它是"安全网"而不是目标状态。
这套机制里最经典的工程方案是内容指纹:静态资源文件名里带内容哈希(如 app.a3f9c2.css),内容一变文件名就变。于是 CSS/JS 可以放心设一年长缓存——反正在你眼里,文件名变了就是新文件,旧文件名的缓存"永远新鲜、永远正确"。html 不缓存,它负责引用新文件名。这就是上面配置的内在逻辑:入口文件零缓存保新鲜,指纹资源超长缓存保性能。
⚠️ 常见坑:给 html 也设了长缓存。用户改版后喊"网站没更新"——他的浏览器还在用缓存里的旧 html,引用着旧资源。html 必须保持"每次都可能问服务器"的弹性。
前两层靠响应头驱动,第三层是 httpd 亲自把生成结果存下来。典型场景:某个动态页(比如 CMS 渲染的栏目页)生成要 300 毫秒,但内容十分钟才变一次——让它每请求都重新生成就是浪费。mod_cache 的写法:
LoadModule cache_module modules/mod_cache.so LoadModule cache_disk_module modules/mod_cache_disk.so CacheEnable disk /news CacheDefaultExpire 600 CacheHeader On # 服务重启不清缓存(键基于URL) CacheIgnoreNoStore On
含义:/news 前缀的响应存到磁盘,默认 600 秒内直接复用。CacheHeader On 会在响应头里加一条标记(X-Cache: HIT 或 MISS),验证命中不用猜:
curl -sI https://www.example.com/news/list | grep -i x-cache # 第一次 HIT 前显示 MISS,第二次起显示 HIT
mod_cache 的两个经典事故必须写进部署检查单。事故一:缓存了不该缓的。个人化内容(登录后的页面、含用户名的响应)被缓存,A 用户看到了 B 用户的数据——这是安全事故而非性能问题。防线:确认应用的动态个人化路径不在 CacheEnable 前缀内;应用对不可缓存响应显式发 Cache-Control: no-store 或 private(mod_cache 尊重这些头)。事故二:只缓存了 200 却不知道 5xx 也会被短存。失败响应被缓存导致故障被放大,用 CacheDisable 与负面 TTL 控制兜底。
启用顺序上的一个冷知识:mod_cache 位于输出过滤链,因此它缓存的内容是压缩前的版本(deflate 在其后),命中后的响应仍会按当前请求头决定压不压——顺序正确时两者不打架,别手工调换过滤顺序。
把四层拼成一套生产策略,以新闻站为例:
验证工具用浏览器开发者面板的网络视图最直观:看每个资源的"from cache / 304 / 200"三种状态占比,就是这套策略的健康指标。
💡 一句话记住本节:最快的响应是没发出的响应,其次是不用重新生成的响应。
下一节看归途的第二道工序:内容怎么瘦身、连接怎么提速。