Redis 缓存击穿、穿透、雪崩:三种失效场景的识别与兜底
Redis 缓存击穿、穿透、雪崩:三种失效场景的识别与兜底
击穿是热点 key 失效、穿透是不存在的 key、雪崩是批量同时失效。三种场景给判断依据、代码级兜底方案与压测验证方法。
三种场景的本质区别
| 场景 | 触发条件 | 数据库压力 | 典型现象 |
|---|---|---|---|
| 击穿 | 单个热点 key 过期 | 瞬时高并发打到同一行 | QPS 突增到数千,全是同一条 SQL |
| 穿透 | 查询不存在的数据 | 每次请求都穿透到 DB | 缓存命中率骤降,DB 出现大量空查询 |
| 雪崩 | 大量 key 同时过期 | 整体流量压到 DB | DB 连接数瞬间打满,响应时间飙升 |
一句话区分:击穿是「一个 key」,穿透是「没有 key」,雪崩是「一批 key」。
一、缓存击穿
现象
某个热点 key(如首页配置、爆款商品)在过期瞬间,所有请求同时打到数据库。
判断
redis-cli --stat
redis-cli info stats | grep keyspace_misses
mysql -e "SHOW PROCESSLIST" | grep -c "SELECT.*hot_key_table"
兜底方案
方案 A:互斥锁(推荐)。只让一个线程去加载,其余等待。
import redis, time, json
r = redis.Redis()
def get_with_mutex(key, loader, ttl=300):
val = r.get(key)
if val:
return json.loads(val)
lock_key = f"lock:{key}"
if r.set(lock_key, "1", nx=True, ex=10):
try:
val = loader()
r.setex(key, ttl, json.dumps(val))
return val
finally:
r.delete(lock_key)
else:
time.sleep(0.05)
return get_with_mutex(key, loader, ttl)
关键点:锁必须设过期时间(ex=10),否则持锁线程崩溃会导致死锁。
方案 B:逻辑过期(不设 TTL,异步刷新)。数据里自带过期时间,过期后异步刷新,当前请求先返回旧值。
适用场景:允许短暂返回旧数据(如商品详情、排行榜)。不允许用于余额、库存等强一致场景。
二、缓存穿透
现象
请求查询一个根本不存在的数据,缓存永远不命中,每次都打到 DB。典型攻击是遍历 ID。
判断
redis-cli info stats | grep -E "keyspace_hits|keyspace_misses"
mysql -e "SHOW GLOBAL STATUS LIKE 'Com_select'"
兜底方案
方案 A:缓存空值。用特殊标记表示「确认不存在」,TTL 设短(60 秒),避免大量无效 key 占满内存。
方案 B:布隆过滤器。启动时把所有存在的 ID 灌进去,查询前先判断。1000 万 ID 只需约 18 MB,误判率可配置。
布隆过滤器的权衡:空间效率高,但误判率无法降到 0,且不支持删除。
方案 C:参数校验。非法 ID 直接拒绝,不进缓存也不进 DB。
三、缓存雪崩
现象
大量 key 在同一时间过期,或 Redis 整体不可用。
判断
redis-cli --scan --pattern "user:*" | head -1000 | while read k; do redis-cli ttl "$k"; done | sort -n | uniq -c
如果大量 key 的 TTL 集中在某个区间,说明是批量设置的。
兜底方案
方案 A:TTL 加随机偏移(最简单有效)。在基础 TTL 上叠加正负 10% 的随机值,原本同时过期的 10000 个 key 会分散到 60 秒窗口内。
方案 B:多级缓存。请求 → 本地缓存(Caffeine/LRU,TTL 10s)→ Redis(TTL 300s)→ DB。Redis 挂了,本地缓存还能挡住大部分请求。
方案 C:熔断降级。Redis 不可用时直接降级到 DB 或返回默认值。
方案 D:Redis 高可用。主从 + Sentinel 切换 10-30 秒,Cluster 秒级。
压测验证
改完代码必须验证,否则只是「看起来修好了」。
wrk -t 10 -c 500 -d 60s --latency "http://localhost:8000/api/hot"
watch -n 1 "mysql -e 'SHOW STATUS LIKE \"Threads_connected\"'"
watch -n 1 "redis-cli info stats | grep keyspace"
验证标准:击穿修复后 DB 连接数从 200+ 稳定在 20 以内;穿透修复后命中率从低于 50% 提升到 95% 以上;雪崩修复后 DB 负载平稳。
一张对照表
| 场景 | 根因 | 首选方案 | 备选 |
|---|---|---|---|
| 击穿 | 热点 key 过期 | 互斥锁 | 逻辑过期 |
| 穿透 | 查不存在的数据 | 缓存空值 | 布隆过滤器 |
| 雪崩 | 批量同时过期 | TTL 随机偏移 | 多级缓存 + 熔断 |
小结
三种场景的兜底逻辑完全不同,用错方案等于没修:给穿透加互斥锁无效,因为每个 key 都不同锁不住;给击穿加布隆过滤器无效,因为 key 是存在的;给雪崩加空值缓存无效,因为问题在批量过期不在单 key。
真正有效的做法是先看监控定位是哪一种,再选对应方案。判断依据:看 Redis 命中率骤降是穿透;看 DB 是否有大量同一条 SQL 是击穿;看 key 的 TTL 是否集中在同一时刻是雪崩。
- 点赞
- 收藏
- 关注作者
评论(0)