导航菜单

缓存穿透、雪崩、击穿

🔴 困难

题目描述

解释什么是缓存穿透、缓存雪崩、缓存击穿?如何解决这些问题?

示例场景

# 场景 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 data

3. 缓存击穿(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()

搜索