缓存穿透、雪崩、击穿
🔴 困难题目描述
解释什么是缓存穿透、缓存雪崩、缓存击穿?如何解决这些问题?
示例场景
# 场景 1:缓存穿透
GET user:999999 # 不存在的用户
# 缓存未命中 → 查询 DB → DB 中也没有
# 每次都查询 DB,DB 压力大
# 场景 2:缓存雪崩
# 大量缓存同时过期
SET user:1 "Alice" EX 3600
SET user:2 "Bob" EX 3600
# ... 10000 个用户,都设置 1 小时过期
# 1 小时后,所有缓存同时失效,大量请求打到 DB
# 场景 3:缓存击穿
# 热点数据过期
GET hot:product:1 # 热门商品
# 缓存过期 → 大量并发请求查询 DB
# DB 瞬间压力增大提示
- 缓存穿透:查询不存在的数据
- 缓存雪崩:大量缓存同时失效
- 缓存击穿:热点数据失效
解法
参考答案 (3 个标签)
缓存穿透 缓存雪崩 缓存击穿
1. 缓存穿透(Cache Penetration)
问题
# 查询不存在的数据
GET user:999999 # 缓存未命中
SELECT * FROM users WHERE id = 999999 # DB 也没有
# 结果:NULL
# 下次请求继续重复此过程
# 问题:
# 1. 每次都查询 DB
# 2. 恶意攻击:大量请求不存在的 key
# 3. DB 压力大,可能崩溃解决方案
方案 1:空值缓存
# 缓存 NULL 值
GET user:999999 # 缓存未命中
SELECT * FROM users WHERE id = 999999 # NULL
SET user:999999 "" EX 600 # 缓存空值,5 分钟过期
# 下次请求
GET user:999999 # 返回 ""
# 优点:简单有效
# 缺点:占用内存、可能有不一致方案 2:布隆过滤器
# 布隆过滤器
# 快速判断 key 是否存在
# 初始化布隆过滤器
BF.ADD user_filter 1 2 3 ... 100000
# 查询前先检查
BF.EXISTS user_filter 999999 # 0(不存在)
# 直接返回,不查询 DB
# 优点:内存占用小
# 缺点:误判(可能误认为存在)方案 3:RPC 互斥
# 只允许一个线程查询 DB
GET user:999999 # 缓存未命中
SETNX user:999999:lock "" EX 10 # 加锁
GET user:999999:lock # 1(加锁成功)
# 查询 DB
SET user:999999 "" EX 600
DEL user:999999:lock
# 其他请求
GET user:999999:lock # 等待或直接返回2. 缓存雪崩(Cache Avalanche)
问题
# 大量缓存同时过期
# 假设有 10000 个用户缓存,都在 2024-01-01 10:00:00 过期
# 10:00:00 之前
GET user:1 # 缓存命中
GET user:2 # 缓存命中
# 10:00:00 之后
GET user:1 # 缓存失效 → 查询 DB
GET user:2 # 缓存失效 → 查询 DB
# ... 大量请求同时查询 DB
# DB 瞬间压力增大,可能崩溃解决方案
方案 1:随机过期时间
# 设置不同的过期时间
SET user:1 "Alice" EX 3600 # 1 小时
SET user:2 "Bob" EX 4200 # 1 小时 10 分钟
SET user:3 "Charlie" EX 3900 # 1 小时 5 分钟
# 代码实现
expire_time = 3600 + random(0, 600) # 1 小时 + 随机 0-10 分钟
SET user:3 "Charlie" EX expire_time
# 优点:简单有效
# 缺点:无法避免同时失效方案 2:缓存预热
# 系统启动时,加载热点数据到缓存
# 定时刷新过期时间
# 脚本
for user in hot_users:
data = db.query(user)
redis.set(f"user:{user.id}", data, ex=3600)
# 优点:避免缓存同时失效
# 缺点:需要维护热点数据列表方案 3:互斥锁
# 缓存失效时,加锁
GET user:1 # 缓存失效
SETNX user:1:lock "" EX 10 # 加锁
# 查询 DB
SET user:1 "Alice" EX 3600
DEL user:1:lock
# 其他请求等待或降级
GET user:1:lock # 等待或返回降级数据方案 4:限流降级
# 限制 DB 查询 QPS
# 超过阈值,直接返回降级数据
if qps > threshold:
return "服务繁忙,请稍后再试"
else:
data = db.query(user)
redis.set(f"user:{user.id}", data, ex=3600)
return data3. 缓存击穿(Cache Breakdown)
问题
# 热点数据过期
# 例如:秒杀商品
# 场景
10000 个并发请求
GET product:1 # 热门商品,缓存过期
# 所有请求同时查询 DB
# DB 压力瞬间增大
# 与缓存雪崩的区别
# 缓存雪崩:大量 key 同时过期
# 缓存击穿:单个热点 key 过期解决方案
方案 1:热点数据永不过期
# 热点数据不设置过期时间
SET product:1 "hot_product"
# 或设置逻辑过期
# 数据中包含过期时间
SET product:1 '{"data":"...","expire":1640995200}'
# 查询时检查过期时间
GET product:1
# {"data":"...","expire":1640995200}
if current_time > expire:
# 异步更新缓存
async_update_cache()
else:
return data方案 2:互斥锁
# 只允许一个线程查询 DB
GET product:1 # 缓存失效
SETNX product:1:lock "" EX 10 # 加锁
GET product:1:lock # 1(加锁成功)
# 查询 DB
SET product:1 "hot_product" EX 3600
DEL product:1:lock
# 其他请求
GET product:1:lock # 等待
# 或返回旧数据(如果允许)方案 3:双缓存
# 两个缓存,不同的过期时间
SET product:1:a "data1" EX 3600 # 1 小时
SET product:1:b "data2" EX 7200 # 2 小时
# 查询逻辑
GET product:1:a # 主缓存
if not exists:
GET product:1:b # 备份缓存
if not exists:
# 查询 DB
SET product:1:a "data1" EX 3600
SET product:1:b "data2" EX 7200
else:
# 异步更新主缓存
async_update_cache()