Nginx-08 缓存机制
上一篇讲的是浏览器端的缓存(让客户端别来请求)。这一篇讲Nginx 自己的缓存(请求来了不回源)。两者是不同层次,配合使用效果最好。
1. proxy_cache 基础
1.1 最小可用配置
http {
# 定义缓存区(必须在 http 层,不能在 server/location 里)
proxy_cache_path /var/cache/nginx/proxy
levels=1:2
keys_zone=my_cache:100m
max_size=10g
inactive=60m
use_temp_path=off;
server {
location /api/ {
proxy_pass http://backend;
proxy_cache my_cache; # 启用哪个缓存区
proxy_cache_valid 200 302 10m; # 2xx/302 缓存 10 分钟
proxy_cache_valid 404 1m; # 404 缓存 1 分钟
proxy_cache_valid any 1m; # 其他状态码
add_header X-Cache-Status $upstream_cache_status; # 便于调试
}
}
}
1.2 proxy_cache_path 参数详解
proxy_cache_path /var/cache/nginx/proxy
levels=1:2
keys_zone=my_cache:100m
max_size=10g
inactive=60m
min_free=1g
manager_files=100
manager_sleep=200ms
manager_threshold=300ms
use_temp_path=off;
| 参数 | 说明 |
|---|---|
| 第一个参数 | 缓存文件的存放目录 |
levels=1:2 |
目录层级:一级目录名 1 个字符,二级 2 个字符 |
keys_zone=名字:大小 |
共享内存区,存缓存 key 的元数据。1MB 约能存 8000 个 key |
max_size |
缓存文件占用的磁盘上限,超了由 cache manager 按 LRU 淘汰 |
inactive |
多久没被访问就删除(不管有没有过期) |
min_free |
磁盘剩余空间低于此值就开始淘汰(1.19.1+) |
use_temp_path=off |
必须设 off,让临时文件直接写在缓存目录里 |
manager_* |
控制 cache manager 的清理节奏,避免清理时 I/O 抖动 |
levels 为什么需要? 缓存文件名是 key 的 MD5 值。如果全放一个目录,几百万个文件会让文件系统的目录查找变得极慢(ext4 虽有 htree 索引但仍不理想)。分层后:
key = "httpGETbackend/api/user/1"
md5 = c0a8f5e2b1d4...3f7a
levels=1:2 → /var/cache/nginx/proxy/a/f7/c0a8f5e2b1d4...3f7a
↑ ↑
倒数第1位 倒数第2-3位
注意是从 MD5 的末尾取字符。levels=1:2 产生 16 × 256 = 4096 个目录,每个目录放几千个文件比较合适;文件更多可以用 levels=2:2(65536 个目录)。
use_temp_path=off 为什么必须设? 默认情况下 Nginx 先把上游响应写到 proxy_temp_path,完成后再 rename 到缓存目录。如果这两个路径不在同一个文件系统上,rename 会退化成跨文件系统的复制,造成额外的磁盘 I/O。设 off 后临时文件直接写在缓存目录下,rename 是同文件系统内的原子操作,几乎零成本。
keys_zone 大小怎么算? 每个 key 的元数据占约 128 字节。100m ≈ 80 万个 key。共享内存满了的后果是开始按 LRU 强制淘汰,即使磁盘还有空间。所以 keys_zone 和 max_size 要匹配:
预估 key 数量 = max_size / 平均响应体大小
keys_zone 大小 = 预估 key 数量 × 128 字节 × 1.5(留余量)
例:max_size=10g,平均响应 20KB → 约 50 万个 key → keys_zone 需要 ≈ 96MB
1.3 缓存状态 $upstream_cache_status
| 值 | 含义 |
|---|---|
MISS |
没命中缓存,回源了,响应已被缓存 |
HIT |
命中缓存,没回源 |
EXPIRED |
缓存存在但已过期,回源取了新的 |
STALE |
用了过期的缓存(proxy_cache_use_stale 生效) |
UPDATING |
用了过期缓存,同时另一个请求正在更新(proxy_cache_use_stale updating) |
REVALIDATED |
缓存过期,向上游验证后发现没变(proxy_cache_revalidate on) |
BYPASS |
被 proxy_cache_bypass 规则跳过,直接回源 |
- |
这个 location 没启用缓存 |
调试时一定要把它加到响应头和日志里:
add_header X-Cache-Status $upstream_cache_status always;
log_format cache '$remote_addr "$request" $status '
'cache=$upstream_cache_status rt=$request_time '
'urt=$upstream_response_time';
统计命中率:
awk '{for(i=1;i<=NF;i++) if($i ~ /^cache=/) print $i}' access.log \
| sort | uniq -c | sort -rn
# 85234 cache=HIT
# 8912 cache=MISS
# 1203 cache=EXPIRED
# 455 cache=BYPASS
# 命中率 = HIT / 总数 ≈ 89%
2. 缓存 key 设计
2.1 默认 key
proxy_cache_key $scheme$proxy_host$request_uri;
# 默认其实是:$scheme$proxy_host$request_uri
这个默认值有个问题:没有包含请求方法。GET 和 HEAD 会共用同一个缓存条目。实践中建议显式指定:
proxy_cache_key "$scheme$request_method$host$request_uri";
2.2 常见的 key 定制
# ① 区分登录用户(每个用户独立缓存)
# ⚠️ 慎用!会导致缓存条目数量爆炸
proxy_cache_key "$scheme$request_method$host$request_uri$cookie_uid";
# ② 区分设备类型(移动端和桌面端返回不同内容)
map $http_user_agent $device {
default "pc";
~*mobile|android "mobile";
}
proxy_cache_key "$scheme$request_method$host$request_uri$device";
# ③ 区分语言
proxy_cache_key "$scheme$request_method$host$request_uri$http_accept_language";
# ④ 忽略无关的查询参数(提高命中率)
# 比如 ?utm_source=xxx 这类营销参数不影响响应内容
map $args $cache_args {
default $args;
~*^(?:.*&)?utm_[^&]*(?:&(?<rest>.*))?$ $rest;
}
proxy_cache_key "$scheme$request_method$host$uri?$cache_args";
# ⑤ 只按路径缓存,完全忽略查询参数
proxy_cache_key "$scheme$request_method$host$uri";
key 设计的核心权衡:key 越细,缓存越准确但命中率越低、条目越多;key 越粗,命中率越高但可能返回错误的内容给用户。
最危险的错误是「把用户相关的响应用粗 key 缓存」:
# ⚠️ 严重安全事故
location /api/user/profile {
proxy_pass http://backend;
proxy_cache my_cache;
proxy_cache_valid 200 10m;
# key 里没有用户标识!
# 结果:用户 A 的个人信息被缓存,用户 B 请求时直接命中,看到 A 的数据
}
规则:带 Set-Cookie、Authorization 或任何用户态信息的响应,默认不要缓存。
Nginx 有个保护机制:上游响应带 Set-Cookie 时默认不缓存。但如果你写了 proxy_ignore_headers Set-Cookie; 就把这层保护关掉了——这个指令要格外小心。
2.3 Vary 头的处理
上游返回 Vary: Accept-Encoding 时,Nginx 会自动把对应的请求头加入 key,为每个变体存独立的缓存条目。
但 Vary: User-Agent 或 Vary: * 会让缓存基本失效(UA 千变万化,每个都是独立条目)。如果上游返回了这种头:
# 让 Nginx 忽略 Vary(确认过内容不真的随 UA 变化再这么做)
proxy_ignore_headers Vary;
3. 什么该缓存、什么不该
3.1 缓存方法与条件
location /api/ {
proxy_pass http://backend;
proxy_cache my_cache;
# 只缓存这些方法(默认只有 GET HEAD)
proxy_cache_methods GET HEAD;
# 至少被请求 N 次才缓存(过滤长尾冷数据,节省磁盘)
proxy_cache_min_uses 2;
# 什么情况跳过缓存直接回源(变量非空且不为 "0" 时跳过)
proxy_cache_bypass $cookie_nocache $arg_nocache $http_x_bypass_cache;
# 什么情况不存入缓存
proxy_no_cache $cookie_nocache $arg_nocache;
}
proxy_cache_bypass 和 proxy_no_cache 的区别:
bypass:读的时候跳过缓存(直接回源),但响应仍会写入缓存no_cache:写的时候不存入缓存
想「完全绕过缓存」要两个都配。
3.2 不缓存登录用户的完整方案
map $http_cookie $skip_cache {
default 0;
~*session_id= 1; # 有 session cookie 的一律不缓存
~*token= 1;
}
# 也把某些路径排除
map $request_uri $skip_cache_uri {
default 0;
~^/api/user/ 1;
~^/api/cart/ 1;
~^/admin/ 1;
}
map "$skip_cache$skip_cache_uri" $no_cache {
default 1;
"00" 0; # 只有两个都是 0 才缓存
}
location /api/ {
proxy_pass http://backend;
proxy_cache my_cache;
proxy_cache_valid 200 5m;
proxy_cache_bypass $no_cache;
proxy_no_cache $no_cache;
add_header X-Cache-Status $upstream_cache_status always;
}
3.3 上游的缓存控制头
默认情况下,上游返回这些头会阻止 Nginx 缓存:
Cache-Control: no-cache/no-store/private/max-age=0Expires是过去的时间Set-Cookie(任何值)Vary: *X-Accel-Expires: 0
想强行缓存要用 proxy_ignore_headers:
# ⚠️ 谨慎使用,你在覆盖上游明确表达的意图
proxy_ignore_headers Cache-Control Expires Set-Cookie X-Accel-Expires;
proxy_cache_valid 200 10m;
更好的做法是让上游正确设置头,而不是在 Nginx 层忽略。 上游可以用 X-Accel-Expires 精确控制 Nginx 的缓存时长,而不影响浏览器缓存:
// Go 后端:让 Nginx 缓存 5 分钟,但浏览器不缓存
c.Header("X-Accel-Expires", "300")
c.Header("Cache-Control", "no-store")
X-Accel-Expires 的优先级最高,且这个头不会被转发给客户端。这是控制 Nginx 缓存最干净的方式。
4. 缓存击穿与雪崩防护
这一节是缓存配置的精华,也是面试的深水区。
4.1 问题:缓存过期瞬间的惊群
一个热点 URL 的缓存过期了,此时有 1000 个并发请求打过来。默认行为是:1000 个请求全部回源,后端瞬间被打爆。这就是缓存击穿。
4.2 proxy_cache_lock —— 只让一个请求回源
location /api/ {
proxy_pass http://backend;
proxy_cache my_cache;
proxy_cache_valid 200 10m;
# 同一个 key 只允许一个请求回源,其他请求等待
proxy_cache_lock on;
# 其他请求最多等多久,超时后自己也回源
proxy_cache_lock_timeout 5s;
# 持有锁的请求最多多久必须完成,否则释放锁让别人来
proxy_cache_lock_age 5s;
}
效果:1000 个请求里只有 1 个回源,其余 999 个在 Nginx 里等着,第一个请求填充缓存后,它们直接读缓存返回。
注意 proxy_cache_lock 是每个 worker 独立还是全局的? 是基于共享内存的全局锁,所有 worker 共享。
代价:等待的请求会占用连接和内存。proxy_cache_lock_timeout 不要设太大。
4.3 proxy_cache_use_stale —— 用过期数据兜底
这是可用性的关键配置:
location /api/ {
proxy_pass http://backend;
proxy_cache my_cache;
proxy_cache_valid 200 10m;
# 后端出问题时,用过期的缓存顶上,而不是给用户 502
proxy_cache_use_stale error timeout invalid_header
http_500 http_502 http_503 http_504
updating;
# 后台异步更新:过期后先返回旧数据,同时后台去回源刷新
# (1.11.10+,相当于 stale-while-revalidate)
proxy_cache_background_update on;
}
updating 这个值特别有价值:缓存正在被某个请求更新时,其他请求直接用旧数据,不用等。配合 proxy_cache_background_update on 就实现了 stale-while-revalidate 语义:
请求到来 → 缓存已过期
↓
立即返回过期的缓存内容(用户零等待,X-Cache-Status: STALE 或 UPDATING)
↓
同时在后台发起子请求回源,更新缓存
↓
下一个请求就拿到新数据了
这是缓存配置里性价比最高的两行。它同时解决了两个问题:
- 缓存过期时用户不用等回源(延迟毛刺消失)
- 后端挂了时用户还能看到(虽然是旧的)内容,而不是 502
4.4 proxy_cache_revalidate —— 省带宽
proxy_cache_revalidate on;
缓存过期后,Nginx 带着 If-Modified-Since / If-None-Match 去回源。上游如果返回 304,Nginx 就只更新缓存的过期时间,不重新下载响应体。
对大响应体(大 JSON、图片)效果明显。状态显示为 REVALIDATED。
4.5 缓存雪崩:过期时间打散
如果一批缓存在同一时刻集体过期(比如批量预热的),会造成瞬间的回源洪峰。
Nginx 没有内置的过期时间随机化,但可以用上游控制:
// Go 后端:给缓存时长加随机抖动
jitter := rand.Intn(120) // 0-120 秒随机
c.Header("X-Accel-Expires", strconv.Itoa(600+jitter))
或者用 OpenResty:
set_by_lua_block $cache_ttl {
return 600 + math.random(0, 120)
}
proxy_cache_valid 200 ${cache_ttl}s; -- 注意:proxy_cache_valid 不支持变量
proxy_cache_valid 不支持变量,所以只能靠上游的 X-Accel-Expires 或 Cache-Control: max-age 来做动态 TTL。
4.6 完整的高可用缓存配置
把上面所有防护组合起来:
http {
proxy_cache_path /var/cache/nginx/proxy
levels=1:2
keys_zone=api_cache:200m
max_size=20g
inactive=2h
use_temp_path=off;
server {
location /api/ {
proxy_pass http://backend;
include snippets/proxy-headers.conf;
proxy_cache api_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 301 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_min_uses 1;
# 防击穿:只让一个请求回源
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
proxy_cache_lock_age 5s;
# 高可用:后端挂了用旧数据顶
proxy_cache_use_stale error timeout invalid_header
http_500 http_502 http_503 http_504
updating;
proxy_cache_background_update on;
# 省带宽
proxy_cache_revalidate on;
# 登录用户不走缓存
proxy_cache_bypass $no_cache;
proxy_no_cache $no_cache;
add_header X-Cache-Status $upstream_cache_status always;
}
}
}
这套配置的效果:
- 正常情况:高命中率,过期时只有一个请求回源
- 缓存过期:用户立刻拿到旧数据,后台静默更新
- 后端全挂:用户仍能看到过期内容(在
inactive期限内),而不是 502
5. 主动清除缓存
5.1 商业版:proxy_cache_purge
# NGINX Plus
map $request_method $purge_method {
PURGE 1;
default 0;
}
location /api/ {
proxy_cache my_cache;
proxy_cache_purge $purge_method;
}
curl -X PURGE http://example.com/api/user/1
5.2 开源版方案一:ngx_cache_purge 模块
git clone https://github.com/FRiCKLE/ngx_cache_purge
./configure --add-module=../ngx_cache_purge ...
location ~ /purge(/.*) {
allow 127.0.0.1;
allow 10.0.0.0/8;
deny all;
proxy_cache_purge my_cache "$scheme$request_method$host$1";
}
curl http://127.0.0.1/purge/api/user/1
5.3 开源版方案二:直接删文件(最实用)
缓存文件名是 key 的 MD5,可以直接算出来删掉:
#!/bin/bash
# purge-cache.sh —— 删除指定 URL 的缓存
CACHE_DIR=/var/cache/nginx/proxy
LEVELS="1:2"
KEY="httpsGETexample.com$1" # 必须和 proxy_cache_key 完全一致
MD5=$(echo -n "$KEY" | md5sum | awk '{print $1}')
# levels=1:2 → 取 MD5 末尾 1 位 / 末尾 2-3 位
L1=${MD5: -1}
L2=${MD5: -3:2}
FILE="$CACHE_DIR/$L1/$L2/$MD5"
if [ -f "$FILE" ]; then
rm -f "$FILE" && echo "purged: $1"
else
echo "not found: $1 (key=$KEY, file=$FILE)"
fi
./purge-cache.sh /api/user/1
批量清除(按内容匹配):
# 找出所有缓存了某个字符串的文件并删除
grep -rl "some-pattern" /var/cache/nginx/proxy/ | xargs rm -f
# 清空整个缓存(不用 reload,Nginx 会自动发现文件不存在)
find /var/cache/nginx/proxy -type f -delete
注意:直接删文件后共享内存里的 key 元数据还在,Nginx 下次访问时发现文件不存在会自动当成 MISS 处理并清理元数据。所以不需要 reload。
5.4 用 OpenResty 实现灵活的 purge
location ~ ^/purge(/.*)$ {
allow 10.0.0.0/8;
deny all;
content_by_lua_block {
local uri = ngx.var[1]
local key = "https" .. "GET" .. ngx.var.host .. uri
local md5 = ngx.md5(key)
local l1 = md5:sub(-1)
local l2 = md5:sub(-3, -2)
local path = "/var/cache/nginx/proxy/" .. l1 .. "/" .. l2 .. "/" .. md5
local ok, err = os.remove(path)
if ok then
ngx.say("purged: ", uri)
else
ngx.status = 404
ngx.say("not found: ", uri, " (", tostring(err), ")")
end
}
}
6. 缓存预热
新上线的缓存节点是空的,第一批流量会全部回源。预热可以避免这个冲击:
#!/bin/bash
# warmup.sh —— 从访问日志里提取热门 URL 并预热
# 1. 从昨天的日志里找出访问量 top 1000 的 URL
zcat /var/log/nginx/access.log-$(date -d yesterday +%Y%m%d).gz \
| awk '{print $7}' \
| grep -E '^/api/' \
| sort | uniq -c | sort -rn | head -1000 \
| awk '{print $2}' > /tmp/hot_urls.txt
# 2. 并发预热(控制并发度,别把后端打爆)
cat /tmp/hot_urls.txt | xargs -P 10 -I{} \
curl -s -o /dev/null -w "%{http_code} %{time_total}s {}\n" \
"http://127.0.0.1{}"
预热时验证效果:
# 预热前后对比命中率
curl -s -o /dev/null -D - http://127.0.0.1/api/hot | grep -i x-cache
7. 多级缓存架构
一个完整的缓存体系是分层的,每一层拦掉一部分流量:
浏览器缓存(Cache-Control)
↓ miss
CDN 边缘节点
↓ miss
Nginx proxy_cache(本篇)
↓ miss
应用层缓存(本地内存 / Redis)
↓ miss
数据库
各层的定位:
| 层 | 命中率 | 适合缓存什么 | TTL |
|---|---|---|---|
| 浏览器 | 高(对单用户) | 静态资源、用户专属数据 | 长(hash 资源)/ 短 |
| CDN | 很高 | 静态资源、公共 API | 中长 |
| Nginx | 中 | 公共 API、页面片段 | 短(分钟级) |
| Redis | 中 | 需要主动失效的业务数据 | 灵活 |
| 本地内存 | 低但极快 | 配置、字典表 | 长 |
Nginx 缓存的定位是「短 TTL、抗突发」。它的优势是零网络开销、零序列化开销;劣势是无法精确失效(不像 Redis 能 DEL key)、多台 Nginx 之间不共享。
所以:
- 需要精确失效的业务数据 → Redis
- 能容忍几分钟延迟的公共数据 → Nginx proxy_cache(TTL 设短一点,靠自然过期)
- 静态资源 → CDN + 浏览器
7.1 用 Nginx 做小文件的本地缓存加速
一个实用场景:Nginx 缓存对象存储(S3/OSS)的内容,减少回源费用和延迟。
proxy_cache_path /var/cache/nginx/oss
levels=2:2
keys_zone=oss:500m
max_size=200g
inactive=30d
use_temp_path=off;
server {
listen 80;
server_name img.example.com;
location / {
proxy_pass https://my-bucket.oss-cn-hangzhou.aliyuncs.com;
proxy_set_header Host my-bucket.oss-cn-hangzhou.aliyuncs.com;
# 不传客户端的这些头给 OSS
proxy_set_header Authorization "";
proxy_set_header Cookie "";
proxy_cache oss;
proxy_cache_key "$uri"; # 图片按路径缓存就够了
proxy_cache_valid 200 30d; # 图片基本不变,缓存很久
proxy_cache_valid 404 1m;
proxy_cache_lock on;
proxy_cache_use_stale error timeout updating http_5xx;
proxy_cache_background_update on;
# 忽略 OSS 返回的缓存控制头
proxy_ignore_headers Cache-Control Expires Set-Cookie;
# 给客户端的缓存头由我们自己控制
expires 30d;
add_header Cache-Control "public";
add_header X-Cache-Status $upstream_cache_status always;
}
}
8. FastCGI / uwsgi / gRPC 缓存
同样的机制,换个前缀:
# PHP-FPM
fastcgi_cache_path /var/cache/nginx/fcgi levels=1:2 keys_zone=php:100m
max_size=5g inactive=60m use_temp_path=off;
location ~ \.php$ {
fastcgi_pass unix:/run/php-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_cache php;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_valid 200 10m;
fastcgi_cache_lock on;
fastcgi_cache_use_stale error timeout updating http_500;
fastcgi_cache_background_update on;
fastcgi_no_cache $no_cache;
fastcgi_cache_bypass $no_cache;
add_header X-Cache-Status $upstream_cache_status always;
}
grpc_cache 不存在——gRPC 是流式的,Nginx 不支持缓存 gRPC 响应。
9. 缓存监控
9.1 关键指标
# 命中率
awk '/cache=HIT/{h++} /cache=/{t++} END{printf "hit rate: %.2f%%\n", h/t*100}' access.log
# 磁盘占用
du -sh /var/cache/nginx/proxy
# 应该接近但不超过 max_size
# 缓存文件数
find /var/cache/nginx/proxy -type f | wc -l
# 共享内存使用情况(需要 NGINX Plus 或第三方模块)
# 开源版只能间接判断:如果磁盘占用远小于 max_size 但一直在淘汰,
# 说明 keys_zone 满了
9.2 用 Prometheus 采集
配合 nginx-vts-module 或 nginx-module-vts:
http {
vhost_traffic_status_zone;
server {
location /status {
vhost_traffic_status_display;
vhost_traffic_status_display_format prometheus;
allow 10.0.0.0/8;
deny all;
}
}
}
输出里包含 nginx_vts_cache_* 系列指标(hit/miss/bypass/expired/stale 的字节数和次数),直接接 Prometheus。
9.3 命中率低的排查清单
| 可能原因 | 怎么确认 |
|---|---|
上游返回 Set-Cookie |
curl -I 看上游响应头 |
上游返回 Cache-Control: no-cache/private |
同上 |
proxy_cache_key 里包含了变化的变量(如 $request_id、时间戳参数) |
检查配置 |
请求 URL 带随机参数(?_=1234567890) |
看 access_log 里的 URI |
keys_zone 太小,一直在 LRU 淘汰 |
磁盘占用远小于 max_size 但 MISS 很多 |
inactive 太短 |
冷门内容还没被再次访问就被删了 |
proxy_cache_min_uses 设得太高 |
检查配置 |
| 请求方法不是 GET/HEAD | 看 access_log |
Vary: User-Agent 导致每个 UA 独立缓存 |
curl -I 看上游响应头 |
10. 面试题
Q:Nginx 的 proxy_cache 是怎么工作的?
按 proxy_cache_key 计算一个 MD5 作为缓存标识,key 的元数据(过期时间、文件名、引用计数)存在 keys_zone 共享内存里,响应体存在磁盘文件里(路径按 levels 分层,从 MD5 末尾取字符)。请求到来时先查共享内存:命中且未过期直接读磁盘文件返回;未命中或过期则回源,同时把响应写入缓存。cache manager 进程按 max_size(LRU)和 inactive 淘汰文件,cache loader 在启动时把已有缓存的元信息载入共享内存。
Q:levels=1:2 是什么意思?为什么需要分层目录?
一级目录用 1 个字符,二级用 2 个字符,字符取自缓存 key MD5 值的末尾。分层是为了避免单个目录下文件过多——几百万个文件在同一目录会让文件系统的查找性能急剧下降。levels=1:2 产生 4096 个子目录。
Q:use_temp_path=off 为什么重要?
默认 Nginx 先把上游响应写到 proxy_temp_path,完成后 rename 到缓存目录。如果两个路径跨文件系统,rename 会退化成完整的文件复制,产生翻倍的磁盘 I/O。设 off 后临时文件直接写在缓存目录内,rename 是同文件系统的原子操作,几乎零开销。
Q:怎么防止缓存击穿(大量并发同时回源)?
proxy_cache_lock on; 让同一个 key 同时只有一个请求回源,其他请求等待(proxy_cache_lock_timeout 控制等待上限)。更好的组合是再加 proxy_cache_use_stale updating; + proxy_cache_background_update on;——过期时立即返回旧数据,后台异步更新,用户完全不用等。
Q:proxy_cache_use_stale 的作用是什么?
在指定条件下(后端 error/timeout/5xx、或缓存正在被更新)允许返回已过期的缓存内容。这是保证可用性的关键:后端全挂时用户仍能看到旧内容而不是 502。配合 proxy_cache_background_update on 就实现了 stale-while-revalidate 语义。
Q:proxy_cache_bypass 和 proxy_no_cache 有什么区别?
proxy_cache_bypass 控制读:条件成立时跳过缓存直接回源,但响应仍会被写入缓存。proxy_no_cache 控制写:条件成立时响应不存入缓存。要完全绕过缓存需要两个都配。
Q:开源版怎么主动清除某个 URL 的缓存?
三种方式:(1) 编译 ngx_cache_purge 第三方模块,提供 PURGE 接口;(2) 按 proxy_cache_key 算出 MD5,直接删对应的缓存文件(不需要 reload,Nginx 下次访问发现文件不存在会自动按 MISS 处理);(3) 用 OpenResty 写一个 Lua 接口做同样的事。官方的 proxy_cache_purge 指令只有 NGINX Plus 有。
Q:keys_zone 设小了会有什么后果?
共享内存放不下更多 key 时会强制按 LRU 淘汰,即使磁盘还远没到 max_size。表现是磁盘占用远低于 max_size 但命中率上不去。估算方法:每个 key 约占 128 字节元数据,keys_zone 大小应该 ≥ 预期 key 数量 × 128 字节 × 1.5。
Q:为什么用户相关的接口不能随便缓存?
如果 proxy_cache_key 里不包含用户标识,用户 A 的响应会被缓存,用户 B 请求同一 URL 时直接命中,看到 A 的数据——这是严重的信息泄露。Nginx 有一层保护:上游返回 Set-Cookie 时默认不缓存,但 proxy_ignore_headers Set-Cookie 会把这层保护关掉,所以这个指令要格外小心。
Q:Nginx 缓存和 Redis 缓存怎么分工?
Nginx 缓存零网络开销、零序列化开销,但无法精确失效、多机不共享,适合短 TTL 的公共内容(靠自然过期)。Redis 支持精确 DEL、多机共享,适合需要主动失效的业务数据。典型分工:公共 API 响应用 Nginx 缓存几分钟抗突发,业务数据用 Redis 并在写操作时主动失效。
上一篇:Nginx-07 动静分离与静态资源优化 | 下一篇:Nginx-09 限流限连与安全防护
xingliuhua