文章评论、排行榜、投票结果这类内容虽然是动态生成的,更新频率却不高,每次请求都打到后端太浪费。把它们缓存在 Nginx 代理层,后端压力和页面加载速度都会明显改善。这篇在 nginx:1.28 容器里把这套配置实跑一遍:命中、过期、后端宕机时的兜底,以及一个会让用户读到别人数据的缓存键错误。

哪些内容适合这样缓存

判据只有一条:动态生成、但更新频率以分钟或小时计。文章评论、排行榜、投票结果都在此列——每次请求实时计算是浪费;反过来,余额、订单状态这类时效敏感数据不适用,缓存换来的一致性代价不划算。

缓存策略

对这类内容推荐两层组合:较长的缓存时间 + 更新时主动清理。用户绝大多数请求命中缓存,内容更新后靠清理动作保证及时可见。缓存时间是”最差情况下用户读到多旧的数据”,清理机制才是实时性的来源,两者缺一不可。

Nginx 配置实例

缓存路径与键(http 块):

1
2
3
4
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=my_cache:10m
max_size=1g inactive=7d use_temp_path=off;

proxy_cache_key "$scheme$request_method$host$request_uri";

keys_zone=my_cache:10m 划出 10MB 共享内存存放缓存键和元数据;max_size=1g 限制缓存总大小;inactive=7d 表示 7 天没被访问就清除;use_temp_path=off 让缓存文件直接写入最终目录,省一次拷贝。

代理与缓存规则(server 块):

1
2
3
4
5
6
7
location /dynamic-content/ {
proxy_pass http://backend;
proxy_cache my_cache;
proxy_cache_valid 200 1h;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
add_header X-Cache-Status $upstream_cache_status;
}

proxy_cache_valid 200 1h 把 200 响应缓存 1 小时;proxy_cache_use_stale 是后端故障时的兜底——后端报错、超时、5xx 时继续提供旧缓存;X-Cache-Status 头暴露每次请求的缓存状态,是调优时最重要的观测点。

实例一:MISS、HIT、EXPIRED 三种状态

容器实测(后端按 query 参数返回内容,TTL 设 3 秒便于观察;是否回源用后端 access.log 行数交叉验证),同一 URL 连续请求并观察响应头:

配图1

第一次 MISS(回源并写入缓存),第二次 HIT(直接命中缓存,内容的时间戳没变——后端没被碰);3 秒 TTL 过后再请求变成 EXPIRED(缓存过期,重新回源并刷新)。X-Cache-Status 的常用取值就是这四个:MISS、HIT、EXPIRED,加上接下来演示的 STALE。

实例二:后端宕机,use_stale 兜底

后端进程被 kill 后再请求同一 URL——缓存已过期、回源必失败,按常理应该是 502:

配图2

实测返回的是 STALE:Nginx 尝试回源失败(error 条件命中 proxy_cache_use_stale),把已过期的旧缓存继续返回给用户。这就是这条配置的价值:后端抖动、重启、发布期间,用户读到的最多是几秒前的旧数据,而不是错误页。对评论、排行榜这类内容,是完全可接受的降级。

错误写法对比:缓存键漏掉参数,用户读到别人的数据

缓存键决定”什么算同一个请求”。把键写成不含参数的形式($uri 而不是 $request_uri):

1
2
3
4
5
# 错:缓存键没有 query string
proxy_cache_key "$scheme$request_method$host$uri";

# 对:$request_uri 自带 query string,天然区分不同参数的请求
proxy_cache_key "$scheme$request_method$host$request_uri";

实测后果:

配图3

先请求 ?id=1(MISS,返回 id=1 的数据),再请求 ?id=2——返回的却是 id=1 的数据,且状态是 HIT。两个不同的请求被当成了同一个,用户 A 的数据串给了用户 B。这类错误在带登录态的接口上就是数据泄露事故;也是为什么要用 $request_uri(完整含参数)做键的基础。

注意事项

  • 缓存时间与更新频率匹配:示例的 1 小时是”用户最多读到 1 小时前的数据”,能接受再设;配合”更新时主动清理”才能兼顾实时性。
  • 主动清理不自带:需要 purge 模块或自研清理脚本(按缓存键删除文件/调接口),上线前先备好,否则内容更新后用户看不到。
  • use_stale 是降级不是常态:它提供的是旧内容,对时效敏感的路径要慎用;配合 updating 参数还能防止缓存过期瞬间的并发回源风暴。
  • 上线后盯 X-Cache-Status:命中率长期偏低先查缓存键和 TTL,再查是否有 Set-Cookie 之类的响应头把响应变成了不可缓存。

小结

回到开头的场景:评论、排行榜这类低频更新内容,用 proxy_cache 三段配置(缓存区、缓存键、缓存规则)就能从后端卸掉大部分重复请求。实跑下来的要点:X-Cache-Status 是眼睛(MISS/HIT/EXPIRED/STALE 四态对应四种链路),use_stale 是后端故障时的安全网,而缓存键必须包含完整的请求参数——漏了就是数据串号事故。


本文由作者 2020-2024 年间的 CSDN 博客文章重构而来,原发布于 CSDN。