【JAVA架构师成长之路】【Redis】第12集:Redis缓存雪崩
30分钟自学教程:Redis缓存雪崩原理与解决方案
目标
- 理解缓存雪崩的成因及危害。
- 掌握预防雪崩的3种核心策略。
- 学会通过代码实现解决方案。
- 能够独立设计应急处理方案。
教程内容
0~2分钟:缓存雪崩的定义与核心原因
- 定义:大量缓存数据同时失效或Redis服务宕机,导致请求直接穿透到数据库,引发数据库崩溃。
- 典型场景:
- 促销活动期间,商品缓存统一设置为1小时后过期。
- Redis主节点故障,且无高可用容灾机制。
2~5分钟:代码模拟雪崩场景(Java示例)
// 初始化时设置相同过期时间(模拟问题根源)
public void initCache() {
List<Product> products = productService.loadAllProducts();
for (Product product : products) {
String key = "product:" + product.getId();
// 所有Key在1小时后同时过期
redisTemplate.opsForValue().set(key, product, 1, TimeUnit.HOURS);
}
}
// 高并发请求触发雪崩
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable String id) {
String key = "product:" + id;
Product product = redisTemplate.opsForValue().get(key);
if (product == null) {
product = productService.loadFromDB(id); // 所有请求同时访问数据库
redisTemplate.opsForValue().set(key, product, 1, TimeUnit.HOURS);
}
return product;
}
5~12分钟:解决方案1——随机过期时间
- 原理:为每个Key设置基础过期时间 + 随机偏移量,分散失效时间。
- 代码实现:
public void setProductCache(Product product) {
String key = "product:" + product.getId();
int baseExpire = 3600; // 基础1小时
int randomExpire = new Random().nextInt(600); // 随机0~10分钟
redisTemplate.opsForValue().set(
key, product, baseExpire + randomExpire, TimeUnit.SECONDS
);
}
- 注意事项:
- 随机范围需根据业务负载调整(如高峰期增大随机区间)。
- 结合LRU/LFU内存淘汰策略,避免内存溢出。
12~20分钟:解决方案2——互斥锁(分布式锁)
- 原理:缓存失效时,仅允许一个线程重建数据,其他线程等待或重试。
- 代码实现(Redisson分布式锁):
public Product getProductWithLock(String id) {
String key = "product:" + id;
Product product = redisTemplate.opsForValue().get(key);
if (product == null) {
RLock lock = redissonClient.getLock("product_lock:" + id);
try {
if (lock.tryLock(3, 30, TimeUnit.SECONDS)) { // 尝试获取锁
product = redisTemplate.opsForValue().get(key); // 双重检查
if (product == null) {
product = productService.loadFromDB(id);
redisTemplate.opsForValue().set(key, product, 3600, TimeUnit.SECONDS);
}
} else {
Thread.sleep(50); // 未获取锁,短暂等待后重试
return getProductWithLock(id);
}
} finally {
lock.unlock();
}
}
return product;
}
- 关键点:
- 锁粒度需精细(如按Key加锁),避免全局锁性能瓶颈。
- 设置锁超时时间,防止死锁。
20~25分钟:解决方案3——多级缓存(Redis + Caffeine)
- 原理:本地缓存(如Caffeine)作为一级缓存,Redis作为二级缓存。
- 代码实现(Spring Boot集成):
// 配置Caffeine本地缓存
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
.maximumSize(1000));
return cacheManager;
}
// 使用两级缓存
@Cacheable(value = "productCache", key = "#id")
public Product getProductWithMultiCache(String id) {
return productService.loadFromDB(id); // 本地缓存未命中时查询Redis及数据库
}
- 优势:
- 本地缓存缓解Redis压力,即使Redis宕机仍能部分提供服务。
- 适合读多写少的热点数据场景。
25~28分钟:应急处理方案
-
熔断降级:
- 使用Sentinel或Hystrix熔断数据库访问,返回默认数据。
@SentinelResource(value = "getProduct", blockHandler = "handleBlock") public Product getProduct(String id) { // 正常业务逻辑... } public Product handleBlock(String id, BlockException ex) { return new Product("默认商品"); }
-
数据预热:
- 在缓存恢复后,主动加载热点数据。
public void preloadHotData() { List<String> hotIds = productService.getHotProductIds(); hotIds.forEach(id -> { Product product = productService.loadFromDB(id); redisTemplate.opsForValue().set("product:" + id, product); }); }
28~30分钟:总结与优化方向
- 核心原则:分散失效时间、降低并发压力、多级容灾。
- 高级优化:
- 监控热点Key,动态调整过期时间。
- 使用Redis Cluster或Sentinel实现高可用。
练习与拓展
练习
- 修改随机过期时间代码,实现动态随机范围(如根据系统负载自动调整)。
- 使用Caffeine实现一个本地缓存,模拟Redis宕机时的降级效果。
推荐拓展
- 学习Redis Cluster的搭建与数据分片原理。
- 研究熔断框架(如Resilience4j)的底层实现。
- 探索缓存穿透、击穿与雪崩的综合解决方案。