缓存穿透
含义:查询一个缓存和数据库中都不存在的数据。
过程:请求到达 → 缓存未命中 → 查询数据库 → 数据库也不存在 → 下次相同请求仍然会继续查询数据库。
危害:大量无效请求会直接打到 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。
刷新方式通常有两种:
-
后台定时任务主动刷新
后台任务周期性查询 DB 并更新 Redis。
-
逻辑过期 + 请求侧异步刷新
在 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 没了。
雪崩:一大片缓存塌了。