理清缓存穿透、缓存击穿、缓存雪崩、缓存不一致的本质与解决方案
在构建高性能系统中,缓存(如Redis) 是不可或缺的关键组件,它大幅减轻了数据库压力、加快了响应速度。然而,在高并发环境下,缓存也可能带来一系列棘手的问题,如:缓存穿透、缓存击穿、缓存雪崩、缓存不一致。
这些问题听起来名字相似,容易混淆。今天这篇文章,就带大家系统地理解它们的本质区别、发生场景及最佳实践方案。
🧩 一图总览
问题 | 类比 | 本质 | 场景 | 是否打DB | 解决方案 |
---|---|---|---|---|---|
缓存穿透 | 故意找错门 | 请求的是不存在的key | 用户请求id=-1 | ✅ 每次 | 布隆过滤器、空值缓存 |
缓存击穿 | 热门商品钥匙丢了 | 热点缓存突然失效 | 秒杀详情失效瞬间访问量激增 | ✅ 高并发打DB | 加锁、逻辑过期 |
缓存雪崩 | 大楼停电 | 大量缓存同时失效 | 设置了相同TTL | ✅ 大规模打DB | 过期时间加随机值、限流 |
缓存不一致 | 仓库和门市不同步 | 缓存与数据库更新顺序错误 | 先更新DB再删缓存,删失败 | ✅ 数据错误 | 先删缓存后更新、消息队列 |
1️⃣ 缓存穿透(Cache Penetration)
📌 本质
指的是请求的数据在缓存和数据库中都不存在,导致每次都穿透缓存打到数据库。
🎯 场景
-
用户频繁访问不存在的 userId,如:
userId = -1
-
恶意攻击脚本请求随机字符串ID,绕过缓存打数据库
✅ 解决方案
-
布隆过滤器:构建合法key集合,提前拦截非法请求
-
空值缓存:对查询结果为空也做短期缓存,避免重复查询
if (result == null) {redis.set(key, "null", 60); // 缓存空值1分钟 }
2️⃣ 缓存击穿(Cache Breakdown)
📌 本质
某个热点数据在失效瞬间被高并发请求,造成短时间内大量请求直击数据库。
🎯 场景
-
秒杀系统中某商品详情缓存5分钟,到期那一刻被数万用户请求。
✅ 解决方案
-
互斥锁:只有一个线程可以查询DB并刷新缓存,其他等待
-
逻辑过期:缓存中放入“过期时间”,即使过期,旧数据仍返回,由后台线程异步刷新缓存(常见于百度方案)
3️⃣ 缓存雪崩(Cache Avalanche)
📌 本质
大量缓存在同一时间过期,大量请求并发击穿缓存,压垮数据库。
🎯 场景
-
代码里为所有缓存设置了固定TTL 30分钟,30分钟后集体过期。
✅ 解决方案
-
过期时间加随机值,错开失效时间
int ttl = 1800 + RandomUtils.nextInt(300); // 1800±300秒 redis.set(key, value, ttl);
-
预热缓存机制:启动时提前加载核心数据
-
限流与熔断机制:保护后端服务
4️⃣ 缓存不一致(Cache Inconsistency)
📌 本质
缓存和数据库中的数据不一致,通常由于更新顺序或更新失败导致。
🎯 场景
-
先更新数据库,再删除缓存,结果删除失败导致缓存是旧值。
-
多线程并发写数据时,缓存更新被覆盖。
✅ 解决方案
-
先删除缓存,再更新数据库(防止“读旧数据”)
-
或 延迟双删策略:更新数据库后延迟再删一次缓存
// 1. 删除缓存 redis.del(key);// 2. 更新数据库 db.update(data);// 3. 延迟再删(可用线程池延迟执行) Thread.sleep(1s); redis.del(key);
-
消息队列异步更新缓存:更新后推送事件,由缓存服务更新
✅ 总结口诀
为了方便记忆,这里提供一句口诀:
穿透找不到,击穿太热闹,雪崩全失效,不一致顺序错。
📌 结语
缓存是提高系统性能的重要手段,但随之也带来了种种挑战。我们在设计系统时,应该对上述四种情况都有应对策略,避免因缓存问题造成服务雪崩、数据异常甚至系统崩溃。
欢迎收藏本篇文章,作为缓存容错设计的备查手册。🚀