第三部分:缓存雪崩------大量key失效引发的"系统性崩溃"
缓存雪崩的本质是"大量缓存key在同一时间失效,或缓存集群整体故障",导致请求全量穿透至DB,引发"系统性崩溃"。
案例4:电商首页的"批量过期"灾难
故障现场
某电商平台首页缓存架构为"Redis集群+MySQL",所有首页商品缓存key(home:item:*
)设置相同过期时间(2小时),每日凌晨2点批量更新缓存。
- 故障:某日凌晨2点,缓存批量过期,首页访问量(5000QPS)全量穿透至MySQL,DB连接池瞬间耗尽,首页无法访问,连带订单、支付等核心服务因依赖首页接口超时,引发系统性崩溃。
根因解剖
- 所有热点key设置相同过期时间,导致"时间点共振";
- 缓存更新采用"先删除旧缓存,再更新DB,最后写入新缓存"的方式,存在"更新窗口"内的穿透风险;
- 未设置多级缓存,Redis故障后无降级方案。
四层防御方案落地
方案1:过期时间"随机化"
核心逻辑:对同一类key设置基础过期时间+随机偏移量(如2小时±10分钟),避免"时间点共振"。
实战代码:
java
@Service
public class HomeCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ItemMapper itemMapper;
// 基础过期时间(2小时)
private static final long BASE_TTL = 7200;
// 随机偏移量范围(±10分钟,即600秒)
private static final int RANDOM_OFFSET = 600;
/**
* 更新首页商品缓存(带随机过期时间)
*/
public void updateHomeItems() {
List<ItemDTO> items = itemMapper.listHomeItems();
// 生成随机数生成器
Random random = new Random();
for (ItemDTO item : items) {
String cacheKey = "home:item:" + item.getId();
// 计算随机过期时间(BASE_TTL ± RANDOM_OFFSET)
long ttl = BASE_TTL + (random.nextInt(2 * RANDOM_OFFSET) - RANDOM_OFFSET);
redisTemplate.opsForValue().set(
cacheKey,
JSON.toJSONString(item),
ttl,
TimeUnit.SECONDS
);
}
}
}
效果:缓存过期时间分散在110-130分钟,避免批量过期,DB峰值QPS从5000降至1500。
方案2:多级缓存架构
核心逻辑:构建"本地缓存(Caffeine)→ Redis → DB"的三级缓存,即使Redis失效,本地缓存仍能拦截部分流量。
实战代码:
java
@Service
public class HomeItemService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ItemMapper itemMapper;
// 本地缓存(Caffeine):首页商品缓存10分钟
private final LoadingCache<String, ItemDTO> localCache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build(this::loadFromRedis);
/**
* 查询首页商品(三级缓存)
*/
public ItemDTO getHomeItem(Long itemId) {
String key = "home:item:" + itemId;
try {
// 1. 查询本地缓存
return localCache.get(key);
} catch (Exception e) {
log.warn("本地缓存未命中,itemId={}", itemId, e);
// 2. 本地缓存失效,直接查询DB并更新各级缓存
ItemDTO item = itemMapper.selectById(itemId);
if (item != null) {
// 更新Redis
redisTemplate.opsForValue().set(key, JSON.toJSONString(item), getRandomTtl(), TimeUnit.SECONDS);
// 更新本地缓存
localCache.put(key, item);
}
return item;
}
}
// 从Redis加载(Caffeine的加载函数)
private ItemDTO loadFromRedis(String key) {
String redisVal = redisTemplate.opsForValue().get(key);
return redisVal != null ? JSON.parseObject(redisVal, ItemDTO.class) : null;
}
// 随机过期时间(同方案1)
private long getRandomTtl() { ... }
}
架构图:
[用户请求] → [本地缓存(Caffeine)] → [Redis集群] → [MySQL]
↓ ↓ ↓
10分钟过期 2小时±10分钟 最终数据源
实战效果:Redis集群故障时,本地缓存拦截60%的请求,DB查询量从5000QPS降至2000QPS,系统未崩溃。
方案3:缓存更新"先更新后删除"
核心逻辑:将"删除旧缓存→更新DB→写入新缓存"改为"更新DB→写入新缓存→删除旧缓存",避免更新窗口内的穿透。
实战代码:
java
@Transactional(rollbackFor = Exception.class)
public void updateItem(ItemDTO item) {
// 1. 先更新DB
itemMapper.updateById(item);
// 2. 写入新缓存(带新版本号)
String newKey = "item:v2:" + item.getId();
redisTemplate.opsForValue().set(newKey, JSON.toJSONString(item), getRandomTtl(), TimeUnit.SECONDS);
// 3. 异步删除旧缓存(避免阻塞主流程)
CompletableFuture.runAsync(() -> {
String oldKey = "item:v1:" + item.getId();
redisTemplate.delete(oldKey);
});
}
效果:缓存更新窗口从1秒缩短至10ms,穿透风险降低99%。
方案4:Redis高可用+熔断降级
核心逻辑:
- Redis集群部署(主从+哨兵),确保单点故障不影响整体;
- 对Redis操作设置熔断(如Resilience4j),故障时快速降级。
实战代码(Redis熔断):
java
@Service
public class RedisServiceWithFallback {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;
/**
* 带熔断的Redis查询
*/
public String getWithFallback(String key) {
CircuitBreaker breaker = circuitBreakerRegistry.circuitBreaker("redisGet");
return Try.ofSupplier(CircuitBreaker.decorateSupplier(breaker, () ->
redisTemplate.opsForValue().get(key)
)).recover(Exception.class, e -> {
log.warn("Redis查询熔断,key={}", key, e);
return null; // 熔断时返回null,触发后续降级逻辑
}).get();
}
}
实战效果:Redis集群单节点故障时,哨兵自动切换(30秒内),熔断机制确保故障期间接口不超时,系统可用性达99.99%。
雪崩防御总结
方案 | 适用场景 | 优点 | 缺点 | 实施成本 |
---|---|---|---|---|
过期时间随机化 | 批量缓存场景 | 实现简单,无额外依赖 | 无法解决集群故障问题 | 低 |
多级缓存 | 核心业务保护 | 多一层防护,性能好 | 一致性维护复杂 | 中 |
更新策略优化 | 缓存频繁更新场景 | 减少更新窗口穿透 | 需要版本号管理 | 中 |
高可用+熔断 | 集群级故障防护 | 兜底保障,可用性高 | 运维成本高 | 高 |
实战总览:缓存故障防御决策树
面对缓存三大劫,需根据业务场景选择合适方案,以下决策树可快速定位防御策略:
-
是否为高频无效key?
→ 是 → 布隆过滤器+缓存空值
→ 否 → 进入下一步
-
是否存在热点key?
→ 是 → 逻辑永不过期/分布式锁
→ 否 → 进入下一步
-
是否有批量过期风险?
→ 是 → 过期时间随机化+多级缓存
→ 否 → 进入下一步
-
是否需极端场景保护?
→ 是 → 限流+熔断降级
→ 否 → 基础缓存策略
缓存防御的核心不是"消灭问题",而是"控制风险"。通过多层防御体系,将故障影响控制在可接受范围,同时平衡性能、一致性和开发成本,才是实战中的最优解。记住:最好的防御方案,永远是最适合业务场景的方案。