如何避免缓存雪崩、缓存击穿和缓存穿透在 Nginx 代理层的发生?
在 Nginx 代理层(通常作为网关或边缘缓存)处理缓存雪崩、击穿和穿透问题,是非常有效的一种架构防御手段。因为它可以将流量拦截在应用服务器和数据库之前。
以下是如何在 Nginx 层面上通过配置和架构设计来避免这三大缓存问题的具体方案:
1. 避免缓存穿透 (Cache Penetration)
现象: 客户端频繁请求一条数据库中根本不存在的数据。由于数据不存在,缓存也不会命中,导致每次请求都直接打到后端应用和数据库,常被黑客用于恶意攻击。
Nginx 层的防御策略:
策略一:缓存空值 / 缓存 404 响应
如果后端返回 404(表示数据不存在),Nginx 可以将这个 404 状态也缓存一小段时间,防止相同的无效请求持续打到后端。plaintext# 缓存 200 正常响应 10 分钟 proxy_cache_valid 200 10m; # 核心:缓存 404 响应 1 分钟(时间不宜过长,防止数据真的新增后无法访问) proxy_cache_valid 404 1m;策略二:请求限流 (Rate Limiting)
针对恶意构造大量不同且不存在的 key 进行攻击的情况,通过 Nginx 的limit_req和limit_conn限制单个 IP 的访问频率。plaintext# 在 http 块定义限流规则:每秒最多 10 个请求 limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s; server { location /api/ { # 允许突发 20 个请求,超过则返回 503 limit_req zone=mylimit burst=20 nodelay; proxy_pass http://backend; } }策略三:结合 OpenResty (Lua) + 布隆过滤器 (Bloom Filter)
这是最彻底的解决方式。利用 OpenResty 在 Nginx 层嵌入 Lua 脚本,请求到达时,先通过 Lua 脚本查询 Redis 中的布隆过滤器。如果布隆过滤器判断 ID 不存在,Nginx 直接返回 404,完全不碰后端。
2. 避免缓存击穿 (Cache Breakdown)
现象: 一个极其热点的数据在缓存中突然过期,此时瞬间有大量的并发请求访问该 key,因为缓存失效,这些请求会同时穿透到后端数据库,导致数据库瞬间压力过大甚至宕机。
Nginx 层的防御策略:
策略一:开启缓存锁 (proxy_cache_lock)
Nginx 提供了合并回源请求的功能。当多个客户端同时请求同一个缓存未命中的 key 时,Nginx 只允许第一个请求去后端拉取数据并更新缓存,其他请求会等待。plaintextlocation /api/ { proxy_cache my_cache; # 开启缓存锁,只允许一个请求回源 proxy_cache_lock on; # 锁超时时间,如果 5 秒内回源请求没回来,放行下一个请求去回源 proxy_cache_lock_timeout 5s; # 如果回源请求超过 5 秒未响应,是否允许其他请求也去回源 (可选) proxy_cache_lock_age 5s; proxy_pass http://backend; }策略二:允许使用过期缓存 (proxy_cache_use_stale updating)
结合后台更新,当缓存过期,第一个请求去后端拉取新数据时,其他的并发请求直接返回旧的过期数据,而不是等待,从而保证极高的并发响应能力。plaintextlocation /api/ { proxy_cache my_cache; # 当缓存正在更新时 (updating),其他请求使用过期缓存 proxy_cache_use_stale updating; proxy_pass http://backend; }
3. 避免缓存雪崩 (Cache Avalanche)
现象: 大量的缓存数据在同一时间集中过期,或者后端服务宕机,导致海量请求瞬间全部打向后端数据库。
Nginx 层的防御策略:
策略一:打散缓存过期时间 (随机过期时间)
Nginx 原生的proxy_cache_valid只能设置固定时间(如10m),无法直接设置随机时间。在 Nginx 层解决这个问题有两种方法:- 依赖后端控制: Nginx 默认会遵守后端应用返回的 HTTP 头(如
Cache-Control: max-age=X)。后端可以在生成响应时,给max-age加上一个随机的抖动值(例如 10分钟 ± 2分钟),Nginx 会按照这个随机时间来缓存。 - OpenResty/Lua 动态设置: 使用 Lua 脚本在 Nginx 层面动态生成并覆盖后端返回的过期时间。
- 依赖后端控制: Nginx 默认会遵守后端应用返回的 HTTP 头(如
策略二:后端故障时的兜底策略 (降级容灾)
如果发生雪崩导致后端应用响应极慢、超时或报错 5xx,Nginx 可以直接将之前缓存的过期数据返回给用户,保证服务可用性(降级)。plaintextlocation /api/ { proxy_cache my_cache; # 当后端返回 error, timeout, 或者 500/502/503/504 时,返回过期的旧缓存! proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504; proxy_pass http://backend; }策略三:后台异步更新缓存 (proxy_cache_background_update)
当客户端请求一个过期的缓存时,Nginx 立即将过期的旧内容返回给客户端,同时在后台偷偷发起一个子请求去后端获取新数据并更新缓存。这彻底消除了客户端因为等待后端响应而产生的延迟,也防止了后端被突发流量打垮。plaintextlocation /api/ { proxy_cache my_cache; # 开启后台更新 proxy_cache_background_update on; # 必须配合 updating 使用,表明在后台更新时,前端使用过期数据 proxy_cache_use_stale updating; proxy_pass http://backend; }
总结:一份高可用的 Nginx 缓存配置模板
综合以上策略,一份防御力较强的 Nginx 代理配置如下:
http {
# 定义缓存路径和规则
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;
# 防穿透:限流规则
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=20r/s;
server {
listen 80;
server_name example.com;
location /api/ {
# 1. 基础缓存配置
proxy_cache my_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
# 2. 防穿透:缓存 404
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
# 2. 防穿透:请求限流
limit_req zone=api_limit burst=50 nodelay;
# 3. 防击穿:开启缓存锁,合并回源请求
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
# 4. 防击穿/防雪崩:允许使用陈旧数据(后台更新时、后端报错时)
proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504;
# 5. 防雪崩:后台异步更新
proxy_cache_background_update on;
# 代理到后端
proxy_pass http://backend_upstream;
}
}
}
通过这套配置,即使后端应用层或 Redis 层面临巨大的瞬间压力,Nginx 代理层也能作为一层坚固的盾牌,依靠缓存锁、异步更新、降级旧数据和空值缓存等机制,确保整个系统的高可用性。