Redis 缓存穿透击穿雪崩
加了缓存后数据库压力骤降,但缓存也有三种“被击穿”的典型故障:穿透(查根本不存在的数据)、击穿(热点 key 过期瞬间被打爆)、雪崩(大量 key 同时失效压垮数据库)。三者名字像、成因不同、解法也不同,本章逐个拆解。
缓存穿透:查不存在的数据
穿透指请求的 key 在缓存和数据库里都不存在,缓存永远拦不住,每次请求都打到数据库,常见于恶意攻击或非法参数。
127.0.0.1:6379> GET user:999999 # 不存在的 id,缓存 miss
# 输出:(nil)
127.0.0.1:6379> GET user:999999 # 每次都 miss,每次都查库
# 输出:(nil)
三种常用解法,可叠加使用:
- 参数校验:id 格式、范围不合法直接拒绝,拦截大部分攻击请求。
- 缓存空值:查不到也写入缓存,值为空占位,TTL 设短(如 60 秒),避免恶意 key 反复穿透:
127.0.0.1:6379> SET user:999999 "" EX 60 NX # 空值也缓存 60 秒
# 输出:OK
- 布隆过滤器:启动时把全部合法 id 装入过滤器,请求先过过滤器,判定“不存在”直接返回。Redis 可用 RedisBloom 模块,也可在应用层用 Guava 等实现:
127.0.0.1:6379> BF.ADD userFilter 10001 # 合法 id 先登记(需 RedisBloom)
# 输出:(integer) 1
127.0.0.1:6379> BF.EXISTS userFilter 999999
# 输出:(integer) 0 # 0 表示一定不存在,直接拦截
缓存击穿:热点 key 过期的瞬间
击穿针对单个热点 key:它正被高并发访问时恰好过期,一瞬间所有请求同时 miss、同时打到数据库。解决思路是“同一时刻只允许一个请求回源重建”:
- 互斥锁:查缓存 miss 后先抢锁,抢到者查库回填,其余线程短暂等待后重查缓存:
# 流程示意:需 import redis, time 并实现 query_db(放入 main 或函数中运行)
r = redis.Redis(host="127.0.0.1", port=6379)
lock_ok = r.set("lock:hot", "1", nx=True, ex=10) # 抢锁成功才回源
if lock_ok:
data = query_db("hot")
r.setex("cache:hot", 300, data) # 重建并回填
r.delete("lock:hot") # 释放锁
else:
time.sleep(0.1) # 没抢到,稍等重试
- 逻辑过期:不给热点 key 设物理 TTL,而在 value 里存“过期时间戳”,读取时发现逻辑过期,先返回旧值,再由后台线程异步重建,用户几乎无感知,适合不要求绝对新鲜的场景。
缓存雪崩:大量 key 同时失效
雪崩的规模大一个量级:缓存中大批 key 在同一时刻过期,或整个缓存实例宕机重启后缓存为空,所有流量瞬间涌向数据库。治理思路是“分散失效时间 + 挡住瞬时流量”:
- 过期时间随机化:固定 TTL 上加随机偏移,避免集体过期:
# 需要 redis-py:pip install redis
import random, redis
r = redis.Redis(host="127.0.0.1", port=6379)
r.setex("news:1", 3600 + random.randint(0, 600), "标题") # 1 小时 + 0~10 分钟随机
- 多级缓存:Redis 前再放一层本地缓存(如 Caffeine/进程内 map),Redis 抖动时由本地缓存顶住。
- 限流与降级:数据库前加限流,超阈值直接降级返回提示,保护数据库不死。
- 高可用:集群 + 主从 + 持久化,避免宕机后缓存“全空”直接重启。
三者对比速查
| 场景 | 特征 | 查库根因 | 核心解法 |
|---|---|---|---|
| 穿透 | 数据根本不存在 | 永远 miss | 参数校验、空值缓存、布隆过滤器 |
| 击穿 | 单个热点 key 过期 | 瞬间集体 miss | 互斥锁、逻辑过期 |
| 雪崩 | 大批 key 同时失效 | 集体 miss | TTL 随机化、多级缓存、限流降级 |
小结:穿透是“查不存在”,用布隆过滤器或空值缓存挡住;击穿是“热点过期瞬间”,用互斥锁或逻辑过期;雪崩是“集体失效”,靠 TTL 随机化、多级缓存与限流降级。它们都要求缓存之外必须有兜底,且数据库侧限流永远值得做。