结构化数据接口的响应往往比静态文件复杂得多:同一个 URL 可能因为 Accept 头返回 JSON 或 XML,可能因为 Authorization 头返回不同租户的数据,也可能因为查询参数顺序不同而形成多个缓存条目。Apache 的 mod_cache 模块本身不会解析 JSON 字段,它更接近一个基于键值对的 HTTP 缓存系统,因此所谓的结构化数据索引,核心在于如何设计缓存键、变体维度和失效策略。

默认缓存键的组成与基础配置
mod_cache 通常与 mod_proxy 配合使用,前者负责存储和检索缓存,后者负责反向代理转发。一个响应能否命中缓存,首先取决于缓存键是否完全一致。Apache 默认的缓存键由请求的协议、主机名、端口、路径和查询字符串组成,这意味着只要这些元素中的任何一个发生变化,就会生成新的索引条目。对于静态资源这种设计很直观,但结构化数据接口往往还带有内容协商头,默认键就显得不够用了。
在开始优化之前,需要先让缓存跑起来。下面是一段典型的磁盘缓存配置,其中 CacheEnable disk / 表示对根路径下的所有资源启用磁盘缓存,CacheRoot 指定缓存文件的存放目录。CacheKeyBaseURL 可以为所有缓存键统一加上一个基础 URL,避免同一台服务器上多个虚拟主机因键冲突而互相污染。
LoadModule cache_module modules/mod_cache.so
LoadModule cache_disk_module modules/mod_cache_disk.so
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
<IfModule mod_cache.c>
CacheEnable disk /
CacheRoot /var/cache/apache2/mod_cache_disk
CacheDefaultExpire 3600
CacheKeyBaseURL http://api.ipipp.com
CacheIgnoreHeaders Set-Cookie
</IfModule>
ProxyPass /api http://127.0.0.1:8080/api
ProxyPassReverse /api http://127.0.0.1:8080/api
这段配置解决了基本缓存,但还没有处理结构化数据的索引问题。默认情况下,如果后端对 Accept: application/json 和 Accept: application/xml 返回不同内容,并且响应中没有声明 Vary,Apache 只会缓存第一次看到的表示,后续请求就可能拿到错误格式。这说明缓存键需要引入更多维度。
Vary 头与内容协商的多维索引
Vary 响应头是 HTTP 内容协商的核心机制,也是 Apache 缓存索引中非常关键的扩展维度。当响应携带 Vary: Accept 时,mod_cache 会把 Accept 头的值纳入缓存变体计算,同一个 URL 下会按照不同的 Accept 值分别存储 JSON、XML 或其他格式的响应。类似地,Vary: Accept-Language 可以区分多语言数据,Vary: Authorization 可以隔离不同用户的私有数据,避免越权命中。
如果后端没有正确返回 Vary 头,可以在 Apache 侧通过 mod_headers 补齐。下面示例对 /api 路径强制添加 Accept 和 Accept-Language 两个变体维度,同时忽略 Set-Cookie 响应头,防止会话 Cookie 导致缓存无法存储或索引碎片化。
<Location /api>
Header add Vary Accept
Header append Vary Accept-Language
CacheIgnoreHeaders Set-Cookie
</Location>
不过 Vary 维度并非越多越好。每增加一个头字段,缓存条目的数量就可能呈指数增长。比如同时区分 Accept、Accept-Language、Authorization、User-Agent,会让索引迅速膨胀,命中率不升反降。合理的做法是只把真正影响响应内容的头加入 Vary,其余头通过 CacheIgnoreHeaders 忽略,或者在后端统一响应格式,减少不必要的表示变体。
查询参数规范化与缓存碎片治理
结构化数据查询接口经常使用 sort、filter、page、fields 等查询参数。这些参数即使内容相同,只要顺序不同,URL 字符串就不同,Apache 默认缓存键就会产生多个条目。例如 /api/data?page=1&sort=asc 和 /api/data?sort=asc&page=1 实际返回相同数据,却各自占用一份缓存,造成碎片。这种情况在缓存容量有限时会显著降低整体命中率。
最简单的治理方案是使用 CacheIgnoreQueryString On 让 Apache 完全忽略查询字符串。但这样做只适合查询参数不影响响应内容的场景,比如一些元数据接口或固定列表。如果必须保留部分查询参数作为索引维度,可以在应用入口层先规范化 URL,例如统一参数顺序、删除无意义参数,再由代理缓存处理。也可以借助 mod_rewrite 把常用参数映射为路径段,使缓存键稳定可读。
RewriteEngine On
RewriteCond %{QUERY_STRING} ^page=(\d+)&sort=(asc|desc)$
RewriteRule ^/api/data$ /api/data/page/%1/sort/%2? [PT]
这个规则把 page 和 sort 两个参数从查询字符串迁移到路径中,查询字符串被问号后的空值去掉,从而让参数顺序不再影响缓存键。需要注意的是,这种重写会改变后端接收到的 URL 路径,因此需要后端配合解析新路径,或者使用 [PT] 标志让重写后的 URL 继续走代理处理,避免后端 404。更复杂的参数组合建议在应用层生成规范化链接,而不是在缓存层过度扭曲 URL。
ETag、Last-Modified 与缓存索引的更新
结构化数据一旦变化,对应的缓存条目需要及时失效或更新。Apache 支持基于验证器的条件请求:当客户端或代理持有旧缓存时,会携带 If-None-Match 或 If-Modified-Since 向源站验证,源站返回 304 表示内容未变,代理继续使用本地缓存体;返回 200 则用新内容替换。ETag 通常比 Last-Modified 更适合结构化数据,因为 JSON 内容可能在极短时间内变更,而 Last-Modified 的时间粒度只能到秒。
在缓存代理层,还可以启用 CacheLock 来防止缓存击穿。当某个热门结构化数据缓存过期时,如果大量请求同时回源,后端压力会瞬间升高。CacheLock 让第一个请求获得锁并回源,其他请求等待锁释放后直接读取新缓存。CacheLockPath 指定锁文件目录,CacheLockMaxAge 控制等待锁的最长时间。
<IfModule mod_cache.c>
CacheEnable disk /
CacheRoot /var/cache/apache2/mod_cache_disk
CacheLock on
CacheLockPath /tmp/mod_cache-lock
CacheLockMaxAge 5
CacheStaleOnError on
</IfModule>
CacheStaleOnError on 表示后端暂时不可用时,Apache 可以继续提供已经过期的缓存数据,避免接口完全不可用。对于结构化数据接口,这意味着即使源数据库或服务抖动,客户端仍能拿到一份旧数据,配合 ETag 的验证机制,可以在恢复后快速收敛到最新内容。这种策略在可用性和新鲜度之间提供了一种可配置的平衡。
总体来说,Apache 代理缓存对结构化数据的索引并不是解析 JSON 内容本身,而是围绕 HTTP 语义建立一套可预测的键值体系。默认缓存键、Vary 变体、查询参数规范化和验证器共同决定了索引的精度与效率。根据接口实际协商头、参数特征和更新频率调整这些配置,才能在命中率、缓存容量和数据新鲜度之间取得合理平衡。
Apache代理缓存结构化数据缓存索引修改时间:2026-09-22 08:36:39