文章目录

Redis缓存穿透、击穿、雪崩的区别与对应解决方案

发布于 2026-04-23 01:22:29 · 浏览 6 次 · 评论 0 条

Redis缓存穿透、击穿、雪崩的区别与对应解决方案

在使用 Redis 缓存架构时,系统通常遵循“先查缓存,缓存未命中则查数据库,回写缓存”的逻辑。然而,在高并发场景下,由于设计不当或异常流量,这种架构会出现三种严重的异常情况:缓存穿透、缓存击穿和缓存雪崩。这三种情况虽然都会导致数据库压力剧增,但其成因和表现形式截然不同。精准识别问题并实施对应的解决方案,是保障系统稳定性的关键。


一、缓存穿透

核心问题:查询一个根本不存在的数据。

由于缓存中没有该数据(通常是因为数据从未写入,或者已被主动清理),请求会直接穿透缓存层,直达数据库。如果此时遭受恶意攻击,例如大量请求查询 ID 为 -1 或不存在的 UUID,数据库会因为频繁处理无效查询而崩溃。

解决方案 1:缓存空对象

当数据库查询结果为空时,不要直接返回空,而是将这个“空结果”缓存起来。

  1. 接收查询请求,例如 product_id = 99999
  2. 查询 Redis 缓存,发现未命中。
  3. 查询 数据库,确认数据不存在。
  4. 写入 Redis,将 key 对应的 value 设置为空值(如 NULL"" 或特定占位符)。
  5. 设置 较短的过期时间(例如 30 秒到 5 分钟),防止缓存占用过多内存。

代码示例:

def get_product(product_id):
    # 1. 查询缓存
    value = redis.get(f"product:{product_id}")
    if value is not None:
        # 即使是空字符串,也直接返回,防止穿透到 DB
        return None if value == "" else deserialize(value)

    # 2. 查询数据库
    product = db.query("SELECT * FROM products WHERE id = %s", product_id)

    # 3. 判断是否存在
    if product:
        redis.setex(f"product:{product_id}", 3600, serialize(product))
        return product
    else:
        # 核心步骤:缓存空值
        redis.setex(f"product:{product_id}", 60, "")
        return None

解决方案 2:布隆过滤器

在访问缓存和数据库之前,先通过一个极其快速的算法判断数据是否可能存在

  1. 初始化布隆过滤器,将所有合法的数据库主键 ID 加载到过滤器中。
  2. 接收查询请求。
  3. 判断布隆过滤器:
    • 如果判断为不存在,直接返回空,拦截请求,绝不让其到达数据库。
    • 如果判断为可能存在,才去查询 Redis 和数据库。

二、缓存击穿

核心问题:某个极度热点的 Key(如秒杀商品、微博热搜)突然过期,且此时有海量并发请求。

与“穿透”不同,“击穿”对应的数据是真实存在的。仅仅是那一瞬间,缓存失效了。成千上万的请求同时发现缓存没货,于是同时涌向数据库,瞬间将其打垮。

解决方案 1:互斥锁

只允许一个线程去查询数据库并重建缓存,其他线程必须等待。

  1. 发现缓存未命中。
  2. 尝试获取分布式锁(如 Redis 的 SETNX)。
  3. 判断获取结果:
    • 获取成功:去数据库查询数据,写入缓存,释放锁。
    • 获取失败:线程休眠一小段时间(如 50ms),然后重试查询缓存。

代码示例:

def get_product(product_id):
    value = redis.get(f"product:{product_id}")
    if value is not None:
        return deserialize(value)

    # 核心步骤:尝试加锁
    lock_key = f"lock:product:{product_id}"
    # setnx: 只有 key 不存在时才设置
    is_locked = redis.setnx(lock_key, 1)

    if is_locked:
        # 加锁成功,设置过期时间防止死锁
        redis.expire(lock_key, 10)
        try:
            product = db.query("SELECT * FROM products WHERE id = %s", product_id)
            redis.setex(f"product:{product_id}", 3600, serialize(product))
            return product
        finally:
            # 释放锁
            redis.del(lock_key)
    else:
        # 加锁失败,等待并重试
        time.sleep(0.05)
        return get_product(product_id) # 递归重试

解决方案 2:逻辑过期

从物理上取消 Key 的自动过期时间,将过期时间存储在 Value 内部。

  1. 设置热点 Key 的 TTL 为 -1(永不过期)。
  2. 结构化存储 Value,包含 data(真实数据)和 expire_time(逻辑过期时间戳)。
  3. 接收请求:
    • 获取数据。
    • 判断 current_time > expire_time
      • 如果未过期,直接返回数据。
      • 如果已过期,开启一个异步后台线程去更新数据库和缓存。
      • 立即返回旧数据给用户(保证用户体验,不阻塞)。

三、缓存雪崩

核心问题大量 Key 在同一时间集中过期,或者 Redis 缓存服务器宕机

由于大量 Key 同时失效,原本落在缓存上的海量请求瞬间全部转发到数据库。这比“击穿”更严重,因为“击穿”通常只针对一个热点 Key,而“雪崩”是成千上万个 Key 同时失效。

解决方案 1:随机过期时间

避免批量设置的 Key 拥有相同的生命周期。

  1. 计算基础过期时间(例如 1 小时)。
  2. 追加一个随机值(例如 0 到 300 秒之间的随机数)。
  3. 设置 Key 的过期时间为 基础时间 + 随机值

公式示例:
$$ T_{final} = T_{base} + \text{Random}(0, 300) $$

代码示例:

import random

base_expire = 3600 # 1小时
random_expire = random.randint(0, 300)
redis.setex(f"product:{product_id}", base_expire + random_expire, serialize(product))

解决方案 2:缓存高可用

防止 Redis 服务器单点故障。

  1. 搭建 Redis 哨兵模式或 Redis 集群。
  2. 配置主从复制,当主节点宕机时,哨兵自动选举新的主节点。
  3. 确保服务对 Redis 故障的降级处理(如限流熔断),即使 Redis 全部挂掉,服务也不应立刻全部崩溃,而是进行限流访问数据库。

四、核心区别对比表

为了方便快速诊断问题,请参考下表对比三种异常的核心特征。

异常类型 核心特征 数据库中的数据 并发量级 典型场景
缓存穿透 查询不存在的数据 不存在 可大可小 恶意攻击、复杂查询误操作
缓存击穿 热点 Key 过期 存在 极高 (海量) 秒杀、突发新闻、热门商品
缓存雪崩 大量 Key 同时失效 存在 极高 (海量) 系统重启后批量加载、凌晨整点失效

五、排查与诊断步骤

当系统数据库 CPU 飙升,响应变慢时,按以下步骤进行排查。

  1. 查看 Redis 监控面板,观察 Hits(命中率)和 QPS
  2. 分析慢查询日志,确定是哪些 SQL 语句在频繁执行。
  3. 检查日志中的查询 Key:
    • 如果查询了大量 ID 看起来像乱码或负数 -> 怀疑是缓存穿透
    • 如果查询集中在某一个特定的合法 ID,且该 Key 刚好过期 -> 怀疑是缓存击穿
    • 如果查询涉及的 ID 覆盖面极广,且时间点集中在整点或系统重启后 -> 怀疑是缓存雪崩
  4. 实施上述对应的临时修复方案(如快速回滚代码、重启 Redis、或加黑名单限流)。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文