云计算百科
云计算领域专业知识百科平台

Redis 缓存穿透、缓存击穿、缓存雪崩到底有什么区别?

我们用 Redis 缓存 MySQL 中的用户信息:

客户端
↓
Redis
↓ 缓存未命中
MySQL
↓
查询结果写入 Redis
↓
返回客户端

正常情况下,大部分请求直接从 Redis 获取数据,只有缓存未命中时才需要访问 MySQL。

但在高并发场景下,可能出现三个非常经典的问题:

缓存穿透、缓存击穿、缓存雪崩。

虽然它们最终都可能导致大量请求进入数据库,但产生原因并不一样,解决方案也不同。


1. 先理解 Cache Aside 模式

在正式介绍三个问题之前,先了解最常见的缓存使用模式:Cache Aside(旁路缓存)。

例如查询用户:

SELECT *
FROM user
WHERE id = 1001;

应用程序一般会先查询 Redis:

GET user:1001
↓
是否命中?
/ \\
是 否
↓ ↓
返回缓存 查询 MySQL
↓
查到数据
↓
写入 Redis
↓
返回结果

这样可以减少数据库查询压力。

但是这里有一个关键问题:

如果大量请求同时无法命中缓存,就会集中访问 MySQL。

接下来三种缓存问题,本质上都与此有关。


2. 什么是缓存穿透?

缓存穿透(Cache Penetration) 指的是:

请求的数据在 Redis 中不存在,在数据库中也不存在,导致请求不断绕过缓存访问数据库。

假设数据库中的用户 ID 是:

1001
1002
1003
…

但是有人不断请求:

GET /user/999999999

这个用户根本不存在。

于是:

客户端
↓
查询 Redis
↓
没有缓存
↓
查询 MySQL
↓
没有数据
↓
返回不存在

下一次再请求同样的 ID:

客户端
↓
Redis 仍然没有
↓
再次查询 MySQL
↓
仍然没有

如果有大量恶意请求不断查询不存在的数据,数据库压力就可能快速上升。

这就是缓存穿透。

需要注意:缓存穿透不是普通的缓存未命中,而是请求的数据在数据库中也不存在,导致缓存无法正常发挥作用。


3. 缓存穿透怎么解决?

最常见的解决办法有三种。

3.1 缓存空值

假设查询:

SELECT *
FROM user
WHERE id = 999999999;

数据库返回空。

正常情况下,应用直接返回不存在。

但我们也可以在 Redis 中缓存一个特殊标记:

SET user:999999999 "__NULL__" EX 120

这样下次再查询相同 ID 时:

查询 Redis
↓
命中 __NULL__
↓
直接返回不存在

不需要再次访问数据库。

所以缓存空值的核心思想是:

即使数据库中没有这条记录,也在缓存中记录“已经确认不存在”。

不过要注意两个问题。

第一,空值的 TTL 通常不应该太长,因为这条数据将来可能被创建。

第二,必须能够区分“Redis 中没有这个 Key”和“Redis 中缓存了不存在标记”。不能简单把两者都当作空值处理。

缓存空值适合实现简单、成本较低的场景,但如果恶意请求使用海量不同 ID,也可能占用很多缓存空间。

3.2 布隆过滤器

Bloom Filter(布隆过滤器) 是一种用于快速判断元素是否可能存在的概率型数据结构。

例如系统中只有以下用户:

1001
1002
1003
1004

我们先把合法用户 ID 加入布隆过滤器。

查询时:

客户端请求 user:999999999
↓
Bloom Filter
↓
判断一定不存在
↓
直接返回

这样就连 Redis 缓存查询和 MySQL 查询都可以避免。

不过布隆过滤器有一个很重要的特点:

  • 判断不存在:在过滤器数据正确、完整的前提下,可以确定不存在。
  • 判断可能存在:不一定真的存在,因为可能发生哈希冲突。

这种误判叫 False Positive(假阳性)。

因此布隆过滤器的作用不是直接代替数据库,而是:

在访问缓存和数据库之前,提前过滤掉大量确定不存在的请求。

另外,新增数据时必须及时更新过滤器,否则新数据可能被错误拦截。普通布隆过滤器也不适合直接删除单个元素,数据删除通常需要额外设计。

3.3 参数校验和限流

例如用户 ID 必须是正整数,那么:

GET /user/-100

这种明显非法的请求,可以直接在业务层拒绝。

还可以通过 Rate Limiting(限流) 限制单个 IP、账号或接口的请求频率。

但限流主要用于保护系统,不能从根本上代替缓存空值和布隆过滤器。


4. 什么是缓存击穿?

缓存击穿(Cache Breakdown) 指的是:

某个访问量非常大的热点 Key 突然失效,导致大量并发请求同时查询数据库。

例如我们缓存了一件热门商品:

SET product:1001 "{…}" EX 1800

商品正在参加秒杀活动。

假设这一刻有大量用户同时请求:

GET /product/1001

平时:

大量请求
↓
Redis product:1001
↓
直接返回

但是当 product:1001 突然过期:

热点 Key 过期
↓
┌─────────────┼─────────────┐
↓ ↓ ↓
请求 A 请求 B 请求 C
↓ ↓ ↓
Redis Miss Redis Miss Redis Miss
↓ ↓ ↓
└─────────────┼─────────────┘
↓
MySQL

大量请求同时进入数据库。

原本 Redis 承担的高并发流量,突然由 MySQL 承担,可能造成数据库连接池耗尽、响应变慢甚至服务不可用。

这就是缓存击穿。

缓存击穿的关键是:一个热点 Key 失效。

而缓存穿透的关键是:数据本身就不存在。


5. 缓存击穿怎么解决?

5.1 互斥锁

最经典的方案就是加锁。

思路很简单:

缓存失效以后,只允许一个请求负责查询数据库并重建缓存,其他请求等待。

例如:

热点 Key 失效
↓
大量并发请求
↓
尝试获取互斥锁
↓
是否获得锁?
/ \\
是 否
↓ ↓
查询 MySQL 等待或稍后重试
↓ ↓
重建 Redis 再次查询 Redis
↓
释放锁

Redis 可以通过:

SET lock:product:1001 requestToken NX PX 5000

尝试获取锁。

这里的 NX 表示 Key 不存在时才设置,PX 5000 表示设置 5 秒过期时间,requestToken 则应是每次加锁操作独有的标识。

但是这里需要注意:

  • 获取锁以后,还需要再次检查缓存,因为可能已有其他请求完成了缓存重建。
  • 释放锁时必须校验锁是否仍然属于自己,不能直接无条件 DEL。
  • 锁过期时间必须考虑数据库查询耗时,防止任务没执行完锁就失效。
  • 没有获取到锁的请求不能无限等待,还需要超时、重试或降级机制。
  • 具体的安全释放、Lua 脚本和锁续期,我们留到后面的 Redis 分布式锁专题。

    5.2 逻辑过期

    还有一种方式叫 Logical Expiration(逻辑过期)。

    普通缓存依赖 Redis 的 TTL:

    SET product:1001 "{…}" EX 1800

    时间到了,Key 就会在逻辑上失效,后续可能被删除。

    逻辑过期则可以把过期时间直接放到缓存对象内部:

    {
    "data": {
    "id": 1001,
    "name": "热门商品"
    },
    "expireAt": "2026-10-08T23:30:00"
    }

    Redis 中的缓存 Key 可以不设置相同的物理 TTL,由应用程序自己判断 expireAt。

    如果没有过期,直接返回。

    如果逻辑上已经过期:

    查询 Redis
    ↓
    发现逻辑过期
    ↓
    返回旧数据
    +
    触发后台缓存重建
    ↓
    更新 Redis

    这样热点缓存不会因为 TTL 到期就立刻消失,大量请求仍然可以读取旧数据。

    优点是可以降低数据库瞬时压力。

    缺点也很明显:

    可能返回过期数据,存在短时间的数据不一致。

    所以逻辑过期适合商品介绍、排行榜等允许一定延迟的场景,不适合要求强一致的实时库存、账户余额等关键数据。

    此外,还要考虑首次没有缓存、后台刷新失败和异常重试等情况,不能认为逻辑过期可以解决所有缓存问题。

    5.3 热点数据提前刷新

    对于可以提前预测的热点 Key,还可以在过期之前主动刷新缓存。

    例如某个热门商品缓存的 TTL 为 30 分钟,可以通过后台任务提前刷新,尽量避免它在高流量期间失效。

    这也叫缓存预热或主动刷新。


    6. 什么是缓存雪崩?

    缓存雪崩(Cache Avalanche) 指的是:

    大量缓存同时失效,或者缓存服务整体不可用,导致大量请求集中进入数据库。

    注意,它和缓存击穿很像,但区别在于影响范围。

    缓存击穿一般是:

    一个热点 Key 失效。

    缓存雪崩一般是:

    大量不同 Key 同时失效,或者 Redis 整体无法提供服务。

    例如:

    product:1001
    product:1002
    product:1003
    product:1004
    product:1005
    …

    这些商品缓存全部设置:

    EXPIRE ... 1800

    如果它们在相近时间创建,就可能在 30 分钟后集中失效。

    于是:

    大量 Key 同时过期
    ↓
    Redis 缓存命中率骤降
    ↓
    大量请求查询 MySQL
    ↓
    数据库压力急剧上升

    这就是缓存雪崩的一种典型场景。

    还有一种情况是 Redis 实例突然宕机。

    原本不同 Key 的请求都能命中缓存,现在整个 Redis 不可用了,流量就可能集中回落到数据库。


    7. 缓存雪崩怎么解决?

    7.1 TTL 增加随机值

    假设有一批商品数据,如果全部设置:

    TTL = 1800 秒

    就可能一起过期。

    因此可以改成:

    TTL = 1800 + random(0, 300)

    这样每个 Key 的过期时间会分散在不同时间点。

    例如:

    product:1001 → 1812 秒
    product:1002 → 1935 秒
    product:1003 → 2078 秒
    product:1004 → 1840 秒

    大量 Key 就不会在同一时刻集中失效。

    所以:

    随机 TTL 主要用于缓解大量 Key 同时过期造成的雪崩。

    需要注意,它不能保证单个热点 Key 不会击穿,也不能解决 Redis 整体宕机。

    7.2 Redis 高可用

    如果是 Redis 服务整体不可用,随机 TTL 显然没有任何作用。

    这种情况应该从 Redis 架构入手。

    例如:

    主从复制

    Sentinel(哨兵)

    Redis Cluster

    通过副本和故障转移等机制提高可用性。

    例如:

    Redis Master
    / \\
    ↓ ↓
    Replica Replica

    如果主节点故障,可以通过相应的高可用机制选择新主节点。

    但高可用不等于永不宕机。故障转移通常存在检测和恢复时间,应用侧仍然需要考虑 Redis 暂时不可用的情况。

    7.3 限流、熔断和降级

    如果 Redis 已经不可用,短时间又无法恢复,数据库可能根本无法承受全部流量。

    这时候需要限制数据库压力。

    例如:

    大量请求
    ↓
    服务端限流
    ↓
    只允许部分请求访问数据库
    ↓
    其他请求返回降级结果

    降级可以根据业务设计成:

    返回稍旧的数据、返回默认内容、提示稍后重试,或者暂时关闭非核心功能。

    核心思想是:

    宁可让部分非关键请求失败,也不要让所有请求把数据库彻底压垮。

    7.4 多级缓存和预热

    对于特别重要的热点数据,可以进一步采用:

    客户端请求
    ↓
    本地缓存(如 Caffeine)
    ↓ 未命中
    Redis
    ↓ 未命中
    MySQL

    这样即使 Redis 短时间不可用,本地缓存仍然可能承担一部分请求。

    但本地缓存也有容量限制和数据一致性问题,不能直接代替 Redis。

    另外,系统启动、大促活动或 Redis 故障恢复后,可以先进行缓存预热,避免所有请求集中进行第一次数据库查询。


    8. 三种缓存问题放在一起比较

    对比缓存穿透缓存击穿缓存雪崩
    数据库是否存在数据 不存在 通常存在 通常存在
    缓存发生什么 查不到数据 单个热点 Key 失效 大量 Key 失效或 Redis 不可用
    主要问题 无效查询持续落到数据库 热点请求瞬时集中 大范围请求集中
    典型解决方法 缓存空值、布隆过滤器 互斥锁、逻辑过期 随机 TTL、高可用、限流降级

    可以通过三个场景快速判断。

    场景一: 用户不断请求不存在的 user:999999999,Redis 和 MySQL 都没有数据,这是缓存穿透。

    场景二: 一个热门商品 product:1001 突然过期,几千个并发请求同时查询 MySQL,这是缓存击穿。

    场景三: 几万个商品缓存同一时间过期,或者 Redis 整体宕机,大量不同请求同时访问 MySQL,这是缓存雪崩。

    需要注意,三种问题在真实系统中可能同时出现。例如雪崩期间,某些特别热门的 Key 也会发生类似击穿的并发重建问题。


    9. 为什么不能所有问题都用互斥锁解决?

    有人可能会想:

    既然加锁能够避免多个请求同时查询数据库,那所有缓存未命中的情况都加锁不就行了吗?

    实际上没有这么简单。

    例如缓存穿透:

    user:9999991
    user:9999992
    user:9999993
    …

    如果每次请求的都是不同的不存在 Key,那么即使按 Key 加锁,也无法阻止大量不同请求进入数据库。

    这种情况更适合参数校验、缓存空值或者布隆过滤器。

    而缓存雪崩如果是 Redis 整体宕机,依赖 Redis 实现的分布式锁本身也可能不可用。

    所以:

    不同缓存问题必须根据产生原因选择解决方案,而不是只记住“缓存异常就加锁”。


    10. 实际项目中怎么组合解决?

    假设我们有一个商品详情接口:

    GET /product/{id}

    可以考虑设计成:

    客户端请求
    ↓
    参数合法性校验
    ↓
    Bloom Filter(按需使用)
    ↓
    查询 Redis
    ↓
    是否命中?
    / \\
    是 否
    ↓ ↓
    返回数据 判断是否热点
    ↓
    协调缓存重建
    ↓
    查询 MySQL
    ↓
    查到数据?
    / \\
    是 否
    ↓ ↓
    写入缓存 缓存空值
    \\ /
    \\ /
    返回结果

    同时,系统还可以在外围增加几层保护:

    • 给大量缓存的 TTL 增加随机偏移,降低集中失效风险。
    • 使用 Redis 高可用机制,减少单节点故障带来的影响。
    • 对数据库访问设置并发上限、超时和降级策略。
    • 对极热门数据采用预热、主动刷新或逻辑过期。

    注意,这是一张组合方案图,不代表所有业务都必须引入布隆过滤器、分布式锁和多级缓存。

    例如一个内部管理系统,用户量很少,简单的 String + TTL + 缓存空值 可能已经足够。

    真正需要高并发防护时,再根据具体瓶颈增加复杂机制。


    11. 最容易搞错的几个地方

    第一,缓存穿透不是所有缓存未命中。 第一次查询某条真实存在的数据时,Redis 没有缓存,需要访问数据库,这是正常的缓存未命中。缓存穿透强调的是数据在数据库中也不存在。

    第二,缓存击穿和缓存雪崩的范围不同。 击穿主要是单个热点 Key,雪崩主要是大量 Key 集中失效或缓存服务整体不可用。

    第三,缓存空值不能设置成永久有效。 数据库中原本不存在的数据可能将来被创建,所以负缓存需要合理 TTL 或主动失效机制。

    第四,布隆过滤器不保证判断存在就真的存在。 它可能产生假阳性,并且必须维护好过滤器与实际数据之间的一致性。

    第五,互斥锁不是只有 SETNX 就够了。 还需要考虑锁过期、二次检查、超时、误删锁和异常释放等问题。

    第六,逻辑过期不是强一致性方案。 它以允许短时间返回旧数据为代价,缓解热点 Key 重建时的数据库压力。

    第七,随机 TTL 主要解决集中到期问题。 如果 Redis 整体宕机,仍然需要高可用、限流和降级等措施。


    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Redis 缓存穿透、缓存击穿、缓存雪崩到底有什么区别?
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!