Redis 缓存学习笔记(三):缓存穿透、缓存击穿、缓存雪崩

缓存穿透

含义:查询一个缓存和数据库中都不存在的数据。

过程:请求到达 → 缓存未命中 → 查询数据库 → 数据库也不存在 → 下次相同请求仍然会继续查询数据库。

危害:大量无效请求会直接打到 DB 层,消耗数据库连接、CPU 和 IO 资源。

缓存穿透一般不是靠单点方案解决,除了常规的参数校验、限流熔断、监控告警外,还可以通过空值缓存、布隆过滤器共同治理。

方案一:缓存空值

当数据库查询结果为空时,也在 Redis 中写入一个特殊空值标记,例如 __NULL__,并设置较短 TTL。后续相同 key 再次请求时,可以直接命中空值缓存,避免重复访问数据库。

优点:实现简单,适合重复查询同一个不存在 key 的场景。

缺点:如果攻击者构造大量随机不存在 key,会在 Redis 中产生大量空值 key,占用存储空间。因此空值缓存一般要设置较短 TTL,并配合参数校验、限流或布隆过滤器。

方案二:布隆过滤器

布隆过滤器维护一个 bit 数组。初始化时,先将数据库中已有的合法业务 key 全量加入过滤器;加入时会对 key 做多次哈希,并把对应 bit 位设置为 1。

请求进来后,先查询布隆过滤器:

  • 如果任意一个哈希位置为 0,说明该 key 一定不存在,可以直接拦截;
  • 如果所有位置都为 1,说明该 key 可能存在,再继续查询 Redis / DB。

优点:空间占用主要与预计合法 key 数量和误判率有关,不会因为大量随机非法请求而产生大量空值 key。

缺点:实现和维护成本更高,需要处理初始化、增量更新、故障恢复、重建和误判率控制。如果真实存在的 key 没有及时加入 Bloom,可能导致正常请求被错误拦截。这里要注意:这不是 Bloom 本身的正常误判,而是初始化或增量同步不完整导致的"漏加入"。

cpp 复制代码
Q:业务对象删除了,布隆过滤器里对应的多哈希 bit 位要不要同步清掉?

A:普通布隆过滤器不删。
因为布隆过滤器的 bit 位是多个 key 共享的,删掉一个对象的 bit 可能把其他真实 key 也误伤,导致正常请求被错误拦截。

普通布隆过滤器宁可放行已删除的脏 key,也不要误删 bit 导致真实 key 被拦截。

Bloom 误判率 = 不存在的 key 被误认为"可能存在"的概率。它只会导致多查一次缓存/DB,不会直接导致真实数据被拦截。如果用 RedisBloom,一般不用自己算太细,可以直接指定容量和误判率。

初始化

布隆过滤器初始 bit 数组全为 0,如果直接启用,会默认认为所有 key 都不存在。因此正式启用前,需要先完成初始化:一般从从库或离线快照中,使用游标分批读取已有业务对象 ID,并写入布隆过滤器。

初始化完成前,应关闭 Bloom 强拦截,让请求正常走缓存/数据库;构建完成并校验后,再开启 bloom_ready

故障恢复

如果使用本地内存布隆过滤器,服务重启后数据会丢失,需要从快照文件或数据库重新加载。如果使用 RedisBloom,则可以复用 Redis 的 RDB + AOF 机制恢复。

但仅依赖 RDB + AOF 仍可能丢失最近一小段 BF.ADD,因此中大型系统通常会结合 outbox、binlog 或 MQ 做增量补偿。

以 outbox 为例:新增业务数据时,在同一个数据库事务中写入业务表和 outbox_event。RedisBloom 恢复后,从一个保守的 outbox_id 起点重新读取新增事件,并对 Bloom 执行 BF.ADD。由于 BF.ADD 是幂等的,可以重复回放,但不能漏回放。补偿完成前,应关闭 Bloom 强拦截。


缓存击穿

含义:某个热点 key 原本在缓存里,但它突然过期了;同一时间大量请求打进来,全部没命中缓存,于是一起去查 DB。

可能导致:

  • DB 连接数被打满
  • 慢查询增多
  • 接口响应变慢
  • 严重时 DB 被打挂

方案一:互斥锁

进程 = 一个正在运行的程序实例。

线程 = 进程里的执行单元。

一个进程里可以有多个线程。

线程会被操作系统调度到 CPU 核心上执行。

互斥锁的本质不是简单地"给 DB 限流",而是控制同一个热点 key 的并发回源,避免大量请求同时访问 DB。它有保护 DB 的效果,但不是传统意义上的限流。

常见做法是通过 Redis 分布式锁,例如:

text 复制代码
SET lock:product:1001 uuid NX EX 10

保证同一时间只有一个请求/执行流回源 DB 并重建该 key 的缓存。

基本流程:

text 复制代码
查询 Redis 未命中
  ↓
尝试获取 key 级分布式锁
  ↓
抢到锁:再次检查缓存 → 查 DB → 写 Redis → 释放锁
  ↓
没抢到锁:短暂 sleep → 重试查缓存
  ↓
超过最大重试次数:降级 / 快速失败 / 返回兜底数据

注意点:

  • 锁粒度应该是 key 级别,例如 lock:product:1001,不要用全局锁;
  • 没抢到锁的请求不能无限 sleep,需要设置最大等待时间和最大重试次数;
  • 抢到锁后要再次检查缓存,避免其他请求已经重建好缓存后自己又重复查 DB;
  • 锁必须设置过期时间,避免持锁线程异常退出导致死锁;
  • 释放锁时要校验 uuid,避免误删其他请求新加的锁,通常用 Lua 脚本保证判断和删除的原子性。

优点:

  • 能有效避免大量请求同时查 DB;
  • 数据一致性相对较好,缓存重建后后续请求可以读到新值。

缺点:

  • 实现稍复杂;
  • 锁等待会增加部分请求延迟;
  • 要注意锁超时、锁释放、最大等待时间和降级策略。

方案二:热点 key 永不过期

对于特别热点的数据,可以不设置 Redis 物理过期时间,让 key 常驻缓存,避免热点 key 突然失效导致大量请求同时打到 DB。

刷新方式通常有两种:

  1. 后台定时任务主动刷新

    后台任务周期性查询 DB 并更新 Redis。

  2. 逻辑过期 + 请求侧异步刷新

    在 value 中维护逻辑过期时间,请求发现数据逻辑过期后,先返回旧值,再通过互斥锁控制只有一个请求异步重建缓存。

定时任务可能因为机器重启、调度异常、代码 bug、任务堆积等原因失效。如果任务挂了,永不过期 key 仍能保证缓存可读,但数据会持续变旧。因此需要任务监控告警,或者配合逻辑过期,让请求侧也具备触发刷新的能力。

逻辑过期可以看作热点 key 永不过期方案的增强版。当请求发现数据逻辑过期时,不直接删除缓存,也不让所有请求回源 DB,而是先返回旧值,并通过互斥锁保证只有一个请求异步重建缓存。所以逻辑过期可以看作是:

text 复制代码
逻辑过期 = key 物理永不过期 + value 内维护逻辑过期时间 + 互斥锁 + 异步重建缓存
方案 刷新触发方式 一致性 优点 缺点
定时任务刷新 后台按固定周期刷新 取决于刷新间隔 简单稳定 可能长时间读旧数据
逻辑过期 请求发现过期后触发刷新 热点 key 下通常更及时 抗击穿能力强,延迟低 会短暂返回旧值

缓存雪崩

含义:大量缓存 key 在同一时间失效,或者 Redis 缓存层整体不可用,导致大量请求绕过缓存直接访问 DB,造成 DB 压力骤增甚至宕机。

缓存雪崩和缓存击穿的区别:

text 复制代码
缓存击穿:单个热点 key 失效
缓存雪崩:大量 key 同时失效,或者 Redis 整体不可用

场景一:大量 key 同时失效

如果一批缓存 key 在同一时间写入,并且设置了相同 TTL,那么它们可能会在同一时间集中失效,导致大量请求同时回源 DB。

解决办法:设置缓存 TTL 时增加随机抖动,例如:

text 复制代码
TTL = baseTTL + random(0, 300s)

这样可以把 key 的过期时间打散,避免同一批 key 在同一时刻集中失效。

另外,也可以配合缓存预热:在活动、秒杀、首页推荐等高流量场景到来前,提前把热点数据加载到 Redis 中。但预热时也要注意 TTL 加随机值,避免下一轮集中失效。

场景二:Redis 整体不可用

Redis 整体挂了、网络异常、主从切换异常、连接池耗尽等情况,都可能导致缓存层整体不可用。

解决办法:提高 Redis 缓存层的高可用能力,例如:

  • Redis 主从复制;
  • Sentinel 哨兵自动故障转移;
  • Redis Cluster 集群;
  • 多副本部署;
  • 必要时使用同城双活、异地多活等容灾方案;
  • 做好连接池隔离、超时控制和故障监控。

兜底保护:限流、熔断、降级

即使做了 TTL 随机值和 Redis 高可用,也不能假设缓存层永远不出问题。因此还需要准备限流、熔断和降级,避免 Redis 异常时所有请求直接把 DB 打挂。

常见做法:

  • 限流:限制回源 DB 的请求量或并发数,超过阈值的请求直接拒绝、排队或返回兜底;
  • 熔断:当 Redis 或 DB 的错误率、超时率过高时,短时间内快速失败,避免继续拖垮系统;
  • 降级:非核心接口返回默认值、静态数据、旧缓存或"系统繁忙,请稍后再试"。

可选增强:多级缓存

对于读多写少、允许短暂不一致的数据,可以使用多级缓存:

text 复制代码
本地缓存 Caffeine / Guava
  ↓
Redis
  ↓
DB

这样 Redis 短暂抖动时,本地缓存还能挡住一部分请求。但多级缓存会增加一致性维护成本,需要结合业务场景选择。


三者总结

问题 访问的数据 典型原因 解决重点
缓存穿透 DB 中不存在的数据 恶意/非法 key 绕过缓存 参数校验、空值缓存、布隆过滤器、限流监控
缓存击穿 DB 中存在的单个热点 key 热点 key 突然过期 互斥锁、逻辑过期、热点 key 永不过期
缓存雪崩 大量 key 或整个缓存层 大量 key 同时过期 / Redis 故障 TTL 随机值、缓存预热、Redis 高可用、限流熔断降级、多级缓存

一句话记忆:

text 复制代码
穿透:查没有的。
击穿:一个热点 key 没了。
雪崩:一大片缓存塌了。