Redis 缓存穿透、击穿、雪崩:三种经典问题的解决方案与实战

作者 lucy · 2026-04-13

⚠️ 声明:本文相关内容仅供参考,实际效果因场景不同可能差异很大,请结合自身情况判断,谨慎参考。

缓存是后端系统的标配,但缓存用不好会引发三种经典问题:穿透、击穿、雪崩。这三个名字听起来吓人,但其实每一种都有成熟的解决方案。本文记录我踩过的坑和对应的处理思路。

场景一:缓存穿透 — 数据库里查不到,还每次都来问

大量请求访问一个根本不存在的数据(比如恶意刷接口),缓存里没有,数据库里也没有,每次都打到 DB,轻则接口变慢,重则 DB 被打死。

判断方法:日志里大量相同 key 查不到,DB 负载异常升高。

解决方案(仅供参考)

方案A:布隆过滤器(BloomFilter)

在访问缓存之前,先用布隆过滤器判断这个 key 存不存在。如果不存在,直接返回,不再查 DB。

# 布隆过滤器判断
def get_user(id):
    if not bloom_filter.contains(id):
        return None  # 一定不存在,直接返回
    # 存在的情况下才查缓存,再查 DB
    cache = redis.get(f"user:{id}")
    if cache:
        return json.loads(cache)
    user = db.query("SELECT * FROM users WHERE id = %s", id)
    if user:
        redis.setex(f"user:{id}", 3600, json.dumps(user))
    return user

方案B:空值缓存

对于已确认不存在的 key,也写入缓存,但设置较短的过期时间。

user = db.query("SELECT * FROM users WHERE id = %s", id)
if not user:
    # 空值缓存5分钟,防止短期内重复查询
    redis.setex(f"user:{id}", 300, "")
else:
    redis.setex(f"user:{id}", 3600, json.dumps(user))
return user

场景二:缓存击穿 — 热点 key 过期,大量请求同时打 DB

某个热点 key 突然过期,而此时有大量并发请求同时涌入,这些请求全部打到数据库,DB 瞬间被压垮。

这和穿透的区别是:数据是存在的,只是 key 过期了。

解决方案(仅供参考)

方案:互斥锁(Mutex)+ 单一提前重建

import threading, time

def get_user(id):
    cache_key = f"user:{id}"
    cache = redis.get(cache_key)
    if cache:
        return json.loads(cache)

    # 加互斥锁,只允许一个请求查DB
    lock_key = f"lock:{id}"
    if redis.set(lock_key, "1", nx=True, ex=10):
        try:
            user = db.query("SELECT * FROM users WHERE id = %s", id)
            redis.setex(cache_key, 3600, json.dumps(user))
            return user
        finally:
            redis.delete(lock_key)
    else:
        # 等待其他请求写入缓存后重试
        time.sleep(0.1)
        return get_user(id)
    return None

也可以用 Redis 的 SETNX + EXPIRE 组合实现分布式锁。

场景三:缓存雪崩 — 大量 key 同时过期,请求全部打到 DB

大量缓存 key 同一时间过期(比如系统重启后、故障恢复后),所有请求同时发现缓存失效,全部涌入 DB。

解决方案(仅供参考)

方案A:过期时间加随机偏移

# 不要所有 key 都设整点过期
expire_time = 3600 + random.randint(0, 300)  # 1小时~1小时05分随机
redis.setex(cache_key, expire_time, value)

方案B:多级缓存(Local + Redis)

本地缓存兜底,Redis 失效时先返回本地缓存数据,避免全部打到 DB。

import cachetools

# 本地LRU缓存,容量100,过期5分钟
local_cache = cachetools.LRUCache(maxsize=100, ttl=300)

def get_user(id):
    if id in local_cache:
        return local_cache[id]
    cache_key = f"user:{id}"
    cache = redis.get(cache_key)
    if cache:
        user = json.loads(cache)
        local_cache[id] = user
        return user
    user = db.query("SELECT * FROM users WHERE id = %s", id)
    if user:
        redis.setex(cache_key, 3600, json.dumps(user))
        local_cache[id] = user
    return user

方案C:Redis 持久化 + 预热

服务重启前,主动从 DB 加载热点数据到缓存,避免冷启动大量击穿。

三种问题对比

问题 原因 核心解法
缓存穿透 数据根本不存在 布隆过滤器 / 空值缓存
缓存击穿 热点 key 过期 互斥锁 + 单一重建
缓存雪崩 大量 key 同时过期 过期时间加随机偏移 + 多级缓存

经验总结

  1. 穿透的根因是”不存在的数据”,优先布隆过滤器。 它空间效率极高,判断不存在的 key 非常快,内存占用是哈希表的 1/10。

  2. 击穿的核心是”并发争抢同一个 key”,互斥锁是标准解法。 注意锁的粒度要细,否则会退化成全局串行。

  3. 雪崩的本质是”时间集中”,加随机偏移是最简单的方案。 改动最小,效果最直接。

  4. 生产环境建议:布隆过滤器 + 多级缓存 + 过期随机偏移 三件套一起上,三种问题同时兜底。

  5. 监控是最后一道防线。 缓存命中率、DB 负载、接口 P99 延迟,这些指标要盯紧,出了问题才能第一时间发现。

⚠️ 再声明:以上方案均有各自适用条件,分布式锁实现复杂度较高,布隆过滤器需要维护额外数据结构,建议根据实际业务量评估后使用。

有问题欢迎留言交流 🚀

发表评论

苏ICP备18039580号-2