在Web性能优化体系中,缓存是降低延迟与节省带宽的核心手段。Apache作为最广泛使用的Web服务器之一,提供了多种模块来控制浏览器端的强缓存与协商缓存行为。强缓存是指浏览器在资源有效期内完全不向服务器发请求,直接读取本地副本;协商缓存则是浏览器每次都发送条件请求,由服务器通过时间戳或内容标识判断资源是否变化,未变化则返回304状态码。这两种机制在Apache中可以通过不同指令组合实现,但配置细节容易混淆,导致缓存失效或更新不及时。

强缓存的配置原理与mod_expires实战
强缓存的实现依赖于响应头中的Expires和Cache-Control。Apache的mod_expires模块能基于文件类型自动计算过期时间,并生成Expires头以及对应的Cache-Control: max-age。其底层逻辑是:当请求到达时,Apache根据配置规则匹配MIME类型,在响应发出前注入未来某个时间点的绝对过期值,同时换算为相对秒数写入Cache-Control。浏览器接到此类头后,在max-age秒内不会再次联系服务器,哪怕用户刷新页面(部分浏览器F5会忽略强缓存,但地址栏回车或外链引用仍生效)。
下面是一个典型的mod_expires配置片段,通常写在虚拟主机或.htaccess中:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 30 days"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType application/javascript "access plus 7 days"
ExpiresByType image/png "access plus 1 year"
</IfModule>
上述配置表示CSS缓存30天,JS缓存7天,图片缓存一年。需要注意access plus是以请求时间为起点计算,若改为modification则基于文件最后修改时间。如果文件更新频繁但配置了一年强缓存,用户将无法及时获取新版本,此时往往需要配合文件名加哈希(如app.abc123.js)来突破缓存。另外,mod_headers可用来更精细地控制Cache-Control的其它指令,例如no-transform或public,避免代理服务器错误转换内容。
使用强缓存的优势是零请求开销,特别适合图标、字体等几乎不变的静态资源。但其缺点也明显:一旦发布急需修正的错误,旧用户可能长期持有错误副本。因此实践中通常对带版本号的文件设长缓存,对入口HTML设短缓存或不缓存,以兼顾速度与更新。
协商缓存的机制与ETag、Last-Modified设置
协商缓存并不阻止浏览器发请求,而是借助Last-Modified与ETag两个头完成验证。Apache默认通过mod_core发送Last-Modified,其值来自文件系统的mtime。当浏览器再次请求时,会带上If-Modified-Since,Apache比对时间,若未改变则返回304。而ETag由mod_file_cache或默认核心生成,一般是文件inode、大小、mtime的哈希,能更精准识别内容变化,因为某些情况(如上传覆盖但mtime不变)下时间戳并不可靠。
以下示例展示如何用mod_headers确保协商头存在,并移除可能引发问题的默认ETag(再自定义):
<IfModule mod_headers.c>
Header unset ETag
FileETag None
Header set Last-Modified "%{LAST_MODIFIED}e" env=LAST_MODIFIED
</IfModule>
在某些集群环境中,多台机器的inode不同会导致同一文件ETag不一致,从而使协商缓存失效并引发重复下载。此时可设置FileETag MTime Size仅用大小和修改时间生成标识,或完全关闭ETag改用Last-Modified。浏览器在收到304后,直接从本地缓存读取旧体,网络传输仅几十字节头部,大幅减少流量。协商缓存适合内容可能变化但又希望复用本地副本的场景,例如用户头像、API JSON(配合正确头)。
需要警惕的是,若同时开启mod_deflate压缩,ETag可能因编码方式不同而被Apache改写为弱验证器(以W/开头),这虽符合HTTP规范,但部分旧CDN兼容性差。此时可关闭压缩对静态文件的ETag影响,或统一在源站处理好再分发。
配置冲突排查与综合策略建议
实际部署时常出现强缓存与协商缓存互相覆盖的情况。例如先由mod_expires写入Cache-Control: max-age=0,随后mod_headers又追加max-age=3600,由于Apache指令合并顺序,最终头可能变成max-age=0, max-age=3600让浏览器无所适从。排查时应使用curl -I查看真实响应头,并确认各模块加载顺序,必要时用Header merge或Header set明确覆盖而非追加。
下面是一段综合配置,对HTML短缓存、对静态资源长强缓存并保留协商头作为兜底:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/html "access plus 0 seconds"
ExpiresByType image/* "access plus 1 year"
</IfModule>
<IfModule mod_headers.c>
<FilesMatch ".(js|css)$">
Header set Cache-Control "max-age=604800, public"
</FilesMatch>
Header merge Cache-Control "no-cache" env=NEED_REVALIDATE
</IfModule>
该策略让图片一年不强请求,JS和CSS一周强缓存但同时允许服务器重新验证。当运维发现某CSS有BUG时,可通过设置环境变量NEED_REVALIDATE让相关请求带no-cache,迫使浏览器走协商,快速修复线上问题而不必改文件名。总体而言,理解Apache各指令生效阶段、合理组合模块,才能构建既高效又可控的缓存体系。
最后强调,任何缓存配置都应在预发布环境用多浏览器验证,尤其注意移动端WebView对Cache-Control的解析差异。只有将强缓存的零延迟与协商缓存的安全性结合,才能在高并发站点中真正提升用户体验。