Redis100篇 - Redis缓存雪崩怎么复现 排查+解决全流程
Redis缓存雪崩复现与解决方案 摘要:本文通过实战演示Redis缓存雪崩现象,分析其危害并提供解决方案。文章首先区分了缓存穿透、击穿和雪崩的概念,随后通过Spring Boot+Redis构建实验环境,模拟10,000个缓存Key同时过期的场景。使用JMeter压测工具触发雪崩后,观察到数据库查询暴增的现象。最后提出4种工业级解决方案:随机TTL、多级缓存、熔断降级和预热机制。文章包含完整的代码

👋 大家好,欢迎来到我的技术博客!
💻 作为一名热爱 Java 与软件开发的程序员,我始终相信:清晰的逻辑 + 持续的积累 = 稳健的成长。
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕Redis这个话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!
文章目录
Redis100篇 - Redis缓存雪崩怎么复现?排查+解决全流程 🌨️💥
在高并发系统中,Redis 作为缓存层,是保护后端数据库的“第一道防线”。然而,一旦这道防线崩溃——比如发生缓存雪崩(Cache Avalanche),大量请求将瞬间穿透到数据库,轻则接口超时、服务降级,重则数据库连接池耗尽、CPU 打满,最终导致整个系统瘫痪。
更可怕的是,缓存雪崩往往来得突然、破坏力强、恢复困难。很多团队直到线上事故爆发,才意识到问题的严重性。
但“纸上得来终觉浅”,只有亲手复现 + 深度排查 + 实战修复,才能真正掌握应对之道。
本文将带你:
✅ 从零搭建一个可复现缓存雪崩的 Java 项目
✅ 模拟高并发下大量 Key 同时过期的场景
✅ 使用监控工具定位雪崩根因
✅ 提供 4 种工业级解决方案 + 完整代码示例
✅ 附赠 Mermaid 可视化流程图 + 可访问外链
无论你是准备面试,还是保障生产系统稳定性,这篇文章都将为你提供可落地、可验证、可复用的实战经验!🚀
🧪 一、什么是缓存雪崩?为什么它如此危险?❄️
📌 定义
缓存雪崩:指在某一时刻,大量缓存 Key 同时失效,导致所有请求直接打到数据库,造成数据库瞬时压力剧增,甚至宕机的现象。
⚠️ 与缓存穿透、击穿的区别
| 问题 | 触发条件 | 请求特征 | 危害程度 |
|---|---|---|---|
| 缓存穿透 | 查询不存在的数据(如 ID=-1) | 恶意/无效请求 | 中(可防御) |
| 缓存击穿 | 单个热点 Key 过期瞬间高并发 | 集中打一个 Key | 高 |
| 缓存雪崩 | 大量 Key 同时过期 | 全局性流量洪峰 | 极高(系统级风险) |
💡 简单记忆:
- 穿透:查不到(DB 无数据)
- 击穿:热点崩(单点失效)
- 雪崩:集体崩(批量失效)
🌰 真实案例
某电商大促期间,商品详情页缓存统一设置 TTL=3600 秒(1小时)。凌晨 00:00:00,数万个商品缓存同时过期。此时恰逢用户活跃高峰,10w QPS 的请求全部穿透到 MySQL,数据库 CPU 瞬间飙至 100%,服务完全不可用,持续 20 分钟。
🔁 二、动手复现:构建缓存雪崩实验环境 🏗️
我们将使用 Spring Boot + Jedis + JMeter 构建一个可复现雪崩的微服务。
🛠 技术栈
- JDK 17
- Spring Boot 3.x
- Redis 7.x(单机即可)
- JMeter 5.6(压测工具)
- Prometheus + Grafana(可选,用于监控)
📂 项目结构
redis-snowstorm-demo/
├── src/main/java/com/example/snowstorm/
│ ├── SnowstormApplication.java
│ ├── controller/ProductController.java
│ ├── service/ProductService.java
│ └── config/RedisConfig.java
├── pom.xml
└── jmeter/snowstorm-test.jmx
1️⃣ Step 1:初始化大量缓存 Key(统一过期时间)
// ProductService.java
@Service
public class ProductService {
@Autowired
private StringRedisTemplate redisTemplate;
// 模拟数据库(实际应为 MyBatis/DataSource)
private final Map<Long, String> db = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
// 预热 10,000 个商品数据到 DB
for (long i = 1; i <= 10_000; i++) {
db.put(i, "Product-" + i + "-Details");
}
// 写入 Redis,TTL 统一设为 10 秒(制造雪崩条件)
for (long i = 1; i <= 10_000; i++) {
redisTemplate.opsForValue().set(
"product:" + i,
db.get(i),
Duration.ofSeconds(10) // ⚠️ 所有 Key 10秒后同时过期!
);
}
System.out.println("✅ 缓存预热完成,10,000 个 Key 将在 10 秒后集体失效!");
}
public String getProductById(Long id) {
String cacheKey = "product:" + id;
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// 模拟数据库查询(加日志便于观察)
System.out.println("🔍 DB 查询: " + id);
String dbData = db.get(id);
if (dbData != null) {
// 重新写入缓存(但未加锁!)
redisTemplate.opsForValue().set(cacheKey, dbData, Duration.ofSeconds(10));
}
return dbData;
}
}
🔥 关键点:
Duration.ofSeconds(10)—— 所有 Key 在同一时间点过期!
2️⃣ Step 2:暴露 HTTP 接口供压测
// ProductController.java
@RestController
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/product/{id}")
public ResponseEntity<String> getProduct(@PathVariable Long id) {
if (id <= 0 || id > 10_000) {
return ResponseEntity.badRequest().body("Invalid ID");
}
String product = productService.getProductById(id);
return product != null ?
ResponseEntity.ok(product) :
ResponseEntity.notFound().build();
}
}
3️⃣ Step 3:启动应用 & Redis
# 启动 Redis(默认配置即可)
redis-server
# 启动 Spring Boot 应用
mvn spring-boot:run
你会看到日志:
✅ 缓存预热完成,10,000 个 Key 将在 10 秒后集体失效!
4️⃣ Step 4:使用 JMeter 模拟高并发请求
创建 jmeter/snowstorm-test.jmx:
- 线程数:500(模拟 500 并发用户)
- Ramp-up 时间:1 秒
- 循环次数:10
- HTTP 请求:
GET http://localhost:8080/product/${__Random(1,10000)}
💡
${__Random(1,10000)}:随机请求 1~10000 的商品 ID
点击 Start,等待 10 秒后(缓存过期瞬间),观察控制台输出。
🔍 三、现象观察:确认缓存雪崩已发生 👀
📊 控制台日志(关键证据)
✅ 缓存预热完成...
(等待 10 秒)
🔍 DB 查询: 5821
🔍 DB 查询: 9342
🔍 DB 查询: 1203
🔍 DB 查询: 7765
...(连续输出数千行 DB 查询日志)...
❗ 正常情况下,每个 ID 只应查一次 DB。但现在每个请求都查 DB,说明缓存完全失效!
📈 监控指标(如有 Prometheus)
- Redis 命中率:从 99% 骤降至 0%
- MySQL QPS:从 10 飙升至 5000+
- 应用 P99 延迟:从 5ms 升至 800ms+
📊 雪崩发生过程(Mermaid)
至此,我们成功复现了缓存雪崩!
🕵️♂️ 四、根因分析:为什么雪崩会发生?🧠
核心原因
- TTL 设置不合理:所有缓存 Key 使用相同的固定过期时间。
- 无重建保护机制:缓存失效后,多个线程同时查 DB 并重建缓存(无互斥)。
- 数据库无熔断:DB 被瞬间打爆,无法自愈。
技术本质
Redis 的过期策略是惰性删除 + 定期删除。当大量 Key 同时过期,Redis 会在短时间内集中清理,但更重要的是——所有客户端请求在同一时刻 Miss 缓存,形成流量洪峰。
🛠 五、解决方案:4 种工业级避坑策略 ✅
✅ 方案一:TTL 加随机值(最简单有效)
原理:在基础 TTL 上增加随机偏移,避免集体过期。
public void setWithRandomTTL(String key, String value) {
long baseExpire = 3600; // 1小时
long randomOffset = new Random().nextInt(600); // ±10分钟
redisTemplate.opsForValue().set(
key,
value,
Duration.ofSeconds(baseExpire + randomOffset)
);
}
📌 适用场景:绝大多数缓存场景,成本低、效果好。
✅ 方案二:互斥锁重建缓存(防 DB 击穿)
原理:只有一个线程能查 DB,其他线程等待或重试。
public String getProductByIdWithLock(Long id) {
String cacheKey = "product:" + id;
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// 尝试获取分布式锁
String lockKey = "lock:product:" + id;
String requestId = UUID.randomUUID().toString();
try {
Boolean locked = redisTemplate.execute(
(RedisCallback<Boolean>) conn ->
conn.set(lockKey.getBytes(), requestId.getBytes(),
Expiration.seconds(3), RedisStringCommands.SetOption.SET_IF_ABSENT)
);
if (Boolean.TRUE.equals(locked)) {
// 查 DB
String dbData = db.get(id);
if (dbData != null) {
redisTemplate.opsForValue().set(
cacheKey, dbData, Duration.ofSeconds(3600 + new Random().nextInt(600))
);
}
return dbData;
} else {
// 未获取到锁,短暂等待后重试(或返回旧数据)
Thread.sleep(50);
return redisTemplate.opsForValue().get(cacheKey); // 再查一次缓存
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 安全释放锁(Lua 脚本保证原子性)
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey), requestId);
}
}
⚠️ 注意:锁的粒度要细(按 ID 锁),避免全局锁影响性能。
✅ 方案三:逻辑过期(永不过期 + 后台更新)
原理:缓存不设物理 TTL,而是存储一个“逻辑过期时间”,由后台线程异步更新。
// 缓存结构:{ "data": "...", "expireTime": 1712345678 }
public void preloadCacheInBackground() {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
for (long i = 1; i <= 10_000; i++) {
String dbData = db.get(i);
Map<String, Object> wrapper = new HashMap<>();
wrapper.put("data", dbData);
wrapper.put("expireTime", System.currentTimeMillis() + 3600_000);
redisTemplate.opsForValue().set("product:" + i, JSON.toJSONString(wrapper));
}
}, 0, 30, TimeUnit.MINUTES); // 每30分钟刷新一次
}
public String getProductWithLogicalExpire(Long id) {
String json = redisTemplate.opsForValue().get("product:" + id);
if (json == null) return null;
JSONObject obj = JSON.parseObject(json);
long expireTime = obj.getLongValue("expireTime");
String data = obj.getString("data");
// 已过期?触发异步更新(但本次仍返回旧数据)
if (System.currentTimeMillis() > expireTime) {
CompletableFuture.runAsync(() -> refreshProduct(id));
}
return data;
}
📌 优点:永不 Miss,用户体验好
📌 缺点:数据可能短暂不一致
✅ 方案四:多级缓存 + 熔断降级(终极防护)
架构:
用户 → L1 本地缓存(Caffeine) → L2 Redis → DB
↓
Sentinel/Hystrix 熔断
// 使用 Caffeine 作为一级缓存
LoadingCache<Long, String> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(id -> loadFromRedisOrDb(id));
private String loadFromRedisOrDb(Long id) {
try {
return productService.getProductByIdWithLock(id);
} catch (Exception e) {
// 熔断:返回兜底数据 or 空
return "Default Product Info";
}
}
配合 Sentinel 配置规则:
- 当 DB 查询异常率 > 50%,自动熔断 10 秒
- 限流:DB 查询 QPS ≤ 1000
📊 四种方案对比(Mermaid)
💡 实际项目中,方案一 + 方案二组合使用最为常见。
📈 六、验证修复效果:再次压测 🧪
修改 ProductService,采用 TTL 随机 + 互斥锁:
public String getProductByIdSafe(Long id) {
String cacheKey = "product:" + id;
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) return cached;
return getProductByIdWithLock(id); // 使用带锁版本
}
// 初始化时也加随机 TTL
redisTemplate.opsForValue().set(
"product:" + i,
db.get(i),
Duration.ofSeconds(10 + new Random().nextInt(5)) // 10~15秒随机
);
重新运行 JMeter 测试:
✅ 控制台日志:每个 ID 仅输出一次 DB 查询
✅ Redis 命中率:维持在 95%+
✅ DB QPS:稳定在 100 以下
雪崩问题彻底解决!
🛡️ 七、生产环境最佳实践清单 📋
- 永远不要设置固定 TTL → 必须加随机偏移(±10%~20%)
- 热点数据单独处理 → 使用永不过期 + 异步更新
- 缓存重建必须加锁 → 避免 DB 被并发打爆
- 关键接口加熔断 → 如 Sentinel、Hystrix
- 监控缓存命中率 → 低于 90% 告警
- 定期演练故障 → 模拟缓存失效,验证系统韧性
🔗 八、延伸阅读(均可正常访问)
💎 结语
缓存雪崩不是“会不会发生”的问题,而是“何时发生”的问题。通过本文的复现 → 排查 → 修复全流程,你不仅掌握了技术方案,更建立了系统性风险防控思维。
记住:优秀的工程师,不是不出问题,而是让问题无法造成灾难。
愿你的系统,永远无雪崩!🎉
🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨
更多推荐


所有评论(0)