缓存穿透、击穿、雪崩是什么?怎么解决?

2025年 阅读约 15 分钟 面试指南 · Redis

深入解析Redis缓存三大问题:缓存穿透(布隆过滤器+空值缓存)、缓存击穿(互斥锁+逻辑过期)、缓存雪崩(随机TTL+多级缓存+熔断降级),附面试模拟问答。

一句话总结

缓存穿透:查不存在的数据,缓存和 DB 都没有 → 布隆过滤器 + 空值缓存缓存击穿:热点 key 过期,大量请求打到 DB → 互斥锁 + 逻辑过期缓存雪崩:大量 key 同时过期或 Redis 宕机 → 随机 TTL + 多级缓存 + 熔断降级。核心口诀:穿透布隆空值防,击穿互斥不过期,雪崩随机多级熔

初级理解

三大问题对比

问题现象原因解决方案
缓存穿透查不存在的数据,每次都穿透到 DB恶意攻击或业务查询不存在的数据布隆过滤器 + 空值缓存
缓存击穿热点 key 过期瞬间,大量请求打到 DB热点 key 过期 + 高并发互斥锁 + 逻辑过期
缓存雪崩大量 key 同时过期或 Redis 宕机TTL 设置相同 + Redis 故障随机 TTL + 多级缓存 + 熔断
# 缓存穿透示例 # 用户请求 id=-1 的商品 GET /product/-1 # → 查 Redis:不存在 # → 查 DB:不存在 # → 返回 null # 每次请求都穿透到 DB! # 缓存击穿示例 # 热点商品 id=100,缓存过期时间 10:00:00 # 10:00:01 瞬间 10000 个请求 # → 查 Redis:已过期 # → 10000 个请求同时查 DB! # → DB 压力骤增
一句话总结:穿透是查不存在的数据,击穿是热点 key 过期,雪崩是大面积过期或宕机。

中级深入

缓存穿透 — 布隆过滤器

布隆过滤器(Bloom Filter)是一种概率型数据结构,用于判断一个元素是否在集合中。特点是:不存在的一定不存在,存在的可能误判

# 布隆过滤器原理 # 1. 初始化一个 bit 数组(全 0) # 2. 添加元素:用 k 个哈希函数计算 k 个位置,置为 1 # 3. 查询元素:计算 k 个位置,全为 1 → 可能存在;有 0 → 一定不存在 # Redis 布隆过滤器(RedisBloom 模块) BF.ADD product_ids 100 # 添加 BF.EXISTS product_ids 100 # 存在 → 1 BF.EXISTS product_ids -1 # 不存在 → 0 # 使用流程 # 1. 预热:将所有商品 ID 加入布隆过滤器 # 2. 请求时先查布隆过滤器 # 3. 不存在 → 直接返回(不查 DB) # 4. 可能存在 → 查缓存 → 查 DB # 空值缓存(兜底方案) # 查 DB 也不存在 → 缓存空值(TTL 短,如 5 分钟) SET product:-1 "" EX 300

缓存击穿 — 互斥锁 vs 逻辑过期

# 方案1:互斥锁(简单,但有等待) public String getProduct(Long id) { String cache = redis.get("product:" + id); if (cache != null) return cache; # 获取锁,只有一个线程查 DB String lockKey = "lock:product:" + id; try { if (redis.setnx(lockKey, "1", 10, TimeUnit.SECONDS)) { # 双重检查 cache = redis.get("product:" + id); if (cache != null) return cache; # 查 DB Product p = db.findById(id); redis.set("product:" + id, p, 30, TimeUnit.MINUTES); return p; } else { Thread.sleep(50); return getProduct(id); # 重试 } } finally { redis.del(lockKey); } } # 方案2:逻辑过期(不阻塞,但数据可能不一致) # 缓存 value 包含数据和过期时间 # 获取缓存 → 判断逻辑过期 → 过期则异步更新 # 返回旧数据(保证可用性)

缓存雪崩 — 随机 TTL + 多级缓存

# 方案1:随机 TTL # 基础 TTL 30 分钟 + 随机 0~5 分钟 int ttl = 30 * 60 + new Random().nextInt(5 * 60); redis.setex("product:" + id, ttl, value); # 方案2:多级缓存 # 本地缓存(Caffeine)→ Redis → DB # 本地缓存 TTL 短(如 1 分钟),Redis TTL 长(如 30 分钟) # 方案3:熔断降级 # 使用 Sentinel/Hystrix # Redis 不可用时 → 熔断 → 返回默认值或限流
中级要点:穿透用布隆过滤器+空值缓存;击穿用互斥锁或逻辑过期;雪崩用随机TTL+多级缓存+熔断。

高级拓展

布隆过滤器参数计算

# 布隆过滤器参数 # n: 预计元素数量 # p: 期望误判率 # m: bit 数组大小 = -n*ln(p) / (ln2)^2 # k: 哈希函数数量 = (m/n) * ln2 # 示例:100 万元素,误判率 0.01% # m = -1000000 * ln(0.0001) / (ln2)^2 ≈ 19170117 bits ≈ 2.3 MB # k = (19170117/1000000) * ln2 ≈ 13 # 不能删除元素(删除可能影响其他元素) # 解决方案:计数布隆过滤器(Counting Bloom Filter)

缓存一致性 — Cache Aside 模式

# Cache Aside 模式(最常用) # 读:先查缓存 → 命中返回 → 未命中查 DB → 写缓存 # 写:先更新 DB → 再删除缓存 # 为什么是删除缓存而不是更新缓存? # 1. 更新缓存可能涉及复杂计算 # 2. 写多读少时,更新缓存浪费 # 3. 并发写可能导致缓存和 DB 不一致 # 延迟双删(解决并发问题) # 1. 删除缓存 # 2. 更新 DB # 3. 延迟一段时间(如 500ms) # 4. 再次删除缓存

实战场景

场景:秒杀商品详情页缓存设计

# 秒杀场景缓存策略 # 1. 预热:活动开始前将商品信息加载到 Redis # 2. 逻辑过期:缓存永不过期,后台异步更新 # 3. 本地缓存:Caffeine 缓存热点商品(1 分钟 TTL) # 4. 限流:网关层限流,保护下游 # 缓存 value 设计 { "id": 100, "name": "秒杀商品", "stock": 50, "expireTime": 1700000000 # 逻辑过期时间 } # 获取逻辑 # 1. 查本地缓存 → 命中返回 # 2. 查 Redis → 命中检查逻辑过期 # 3. 逻辑过期 → 返回旧数据 + 异步更新 # 4. Redis 未命中 → 互斥锁查 DB

面试模拟

面试官:缓存穿透、击穿、雪崩有什么区别?怎么解决?

你:穿透是查不存在的数据,用布隆过滤器+空值缓存;击穿是热点 key 过期,用互斥锁或逻辑过期;雪崩是大面积 key 同时过期或 Redis 宕机,用随机 TTL+多级缓存+熔断降级。口诀:穿透布隆空值防,击穿互斥不过期,雪崩随机多级熔。

面试官:布隆过滤器原理是什么?

你:布隆过滤器是一个 bit 数组 + k 个哈希函数。添加元素时计算 k 个位置置为 1;查询时计算 k 个位置,全为 1 则可能存在,有 0 则一定不存在。特点是:不存在的一定不存在,存在的可能误判。不能删除元素(会影响其他元素),可以用计数布隆过滤器解决。