缓存穿透、击穿、雪崩的成因与解决方案

举报
yd_268144123 发表于 2026/11/13 09:44:45 2026/11/13
【摘要】 一、为什么缓存会“出问题”缓存是性能利器,但把流量挡在数据库前面后,一旦缓存层异常,问题会瞬间放大到数据库。面试和实战里最常被问的三个经典问题——缓存穿透、缓存击穿、缓存雪崩——名字相近,成因和解法完全不同,必须分清。 二、缓存穿透(Cache Penetration)成因:查询一个根本不存在的数据(如 id = -1 或随机不存在的 key)。缓存里没有,数据库里也没有,于是每次请求都穿...

一、为什么缓存会“出问题”

缓存是性能利器,但把流量挡在数据库前面后,一旦缓存层异常,问题会瞬间放大到数据库。面试和实战里最常被问的三个经典问题——缓存穿透、缓存击穿、缓存雪崩——名字相近,成因和解法完全不同,必须分清。

二、缓存穿透(Cache Penetration)

成因:查询一个根本不存在的数据(如 id = -1 或随机不存在的 key)。缓存里没有,数据库里也没有,于是每次请求都穿过缓存打到数据库。恶意攻击者可以构造大量不存在的 key 打垮数据库。

解法:

  1. 缓存空值:查不到也把 null/空标记写入缓存(设较短过期时间,如 60 秒),下次直接命中空结果。
  2. 布隆过滤器(Bloom Filter):把所有存在的 key 放进布隆过滤器,请求先过一遍,明确不存在的直接拦截,不查库。注意布隆过滤器有极小误判率(只会误判“存在”,不会漏判),且不支持删除(要用计数版或定期重建)。
  3. 参数校验:入口做合法性校验(id 必须为正整数、格式校验),挡掉明显非法的请求。

三、缓存击穿(Cache Breakdown)

成因:某个热点 key 过期的一瞬间,大量并发请求同时发现缓存失效,全部涌向数据库去重建这个 key。注意:数据是存在的,只是缓存刚好过期,和“穿透”不同。

解法:

  1. 互斥锁(Mutex):只允许一个请求去数据库加载并回填缓存,其余请求等待/重试。用 Redis SET NX 做锁即可。
  2. 逻辑过期:缓存里不设物理过期,而存一个逻辑过期时间字段;发现逻辑过期后,由一个线程异步去重建,其它请求先返回旧值(保证可用性)。适合允许短暂旧数据的场景。
  3. 热点 key 永不过期 + 后台刷新:对极热数据不设 TTL,靠后台任务定期更新。

四、缓存雪崩(Cache Avalanche)

成因:大量 key 在同一时间集中过期,或缓存服务整体宕机,导致所有请求瞬间打到数据库。和“击穿”的区别是:击穿是单个热点 key,雪崩是大量 key 同时失效或整个缓存层挂了。

解法:

  1. 过期时间加随机抖动:给 TTL 加一个随机值(如 30min + random(0~5min)),避免同一时刻集体过期。
  2. 缓存高可用:Redis 用主从 + 哨兵 / 集群,避免单点宕机。
  3. 限流降级:数据库前加限流,缓存不可用时走降级逻辑(返回兜底数据/默认值),保护数据库。
  4. 多级缓存:本地缓存(Caffeine)+ 分布式缓存(Redis),一层挂了还有另一层缓冲。
  5. 预热:启动或活动前提前把热点数据加载进缓存,避免冷启动瞬间打库。

五、三者对比

问题 数据是否存在 触发条件 核心解法
穿透 不存在 查不存在的 key 缓存空值 / 布隆过滤器
击穿 存在(热点) 单个热点 key 过期 互斥锁 / 逻辑过期
雪崩 存在(大量) 大量 key 同时过期或缓存宕机 随机 TTL / 高可用 / 限流降级

六、代码示例:互斥锁防击穿

def get_user(user_id):
    key = f"user:{user_id}"
    val = redis.get(key)
    if val is not None:
        return val
    # 缓存失效,尝试抢锁重建
    lock_key = f"lock:{key}"
    if redis.set(lock_key, "1", nx=True, ex=10):
        try:
            # 双重检查,避免拿到锁前别人已回填
            val = redis.get(key)
            if val is not None:
                return val
            user = db.query_user(user_id)
            redis.set(key, user, ex=300)
            return user
        finally:
            redis.delete(lock_key)
    else:
        # 没抢到锁,短暂等待后重试读缓存
        time.sleep(0.05)
        return get_user(user_id)

七、常见坑

  1. 把三个概念混为一谈:用错解法,比如用布隆过滤器解决雪崩(无效)。
  2. 缓存空值不设过期:空值永久占用内存且掩盖数据后续新增。
  3. 互斥锁不设过期:持锁进程崩溃导致死锁,必须带 TTL。
  4. 逻辑过期返回旧值不设上限:重建失败时一直返回很旧的数据,要有兜底。
  5. TTL 全一致:批量写入缓存时用了同一个 TTL,等于埋下雪崩隐患。

八、总结

  • 穿透:防“查不存在的”,用缓存空值 + 布隆过滤器。
  • 击穿:防“热点 key 过期瞬间并发”,用互斥锁 / 逻辑过期。
  • 雪崩:防“大面积失效或缓存宕机”,用随机 TTL + 高可用 + 限流降级 + 多级缓存。

一句话记忆:穿透是“没有也查”,击穿是“热点过期”,雪崩是“集体失效”。分清场景再选方案,才能真正守住数据库。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。