缓存雪崩是必然发生的系统风险,必须通过Redis随机TTL、Sentinel熔断降级、本地缓存三层防护协同应对:Redis负责缓存响应,Sentinel作为总阀门控制流量,Caffeine提供本地快速兜底。

缓存雪崩在微服务中不是“会不会发生”的问题,而是“什么时候发生、影响多大”的问题。单靠 Redis 设置随机过期时间或加哨兵,无法覆盖服务调用链断裂、数据库回源失败、热点 Key 突然击穿等真实场景;必须让 Sentinel 和 Redis 在职责上分层协作:Redis 负责数据缓存与快速响应,Sentinel 负责流量拦截、熔断兜底和资源保护。
为什么不能只靠 Redis 的过期策略防雪崩
统一 TTL + Redis 宕机 = 雪崩温床。即使你给 setex 加了随机偏移(比如 60 + rand.Intn(30)),也解决不了以下三类硬伤:
- Redis 实例彻底不可达时,所有
@Cacheable方法直接 fallback 到数据库,Sentinel 若未介入,数据库瞬间被打满 - 缓存未命中后重建逻辑无锁(如多个请求同时查 DB 并写 Redis),引发重复查询与数据库压力倍增
- 数据库本身响应变慢(如慢 SQL、连接池耗尽),但 Redis 层仍持续放行请求,Sentinel 缺位导致线程池被占满
换句话说:Redis 是“缓存管道”,Sentinel 是“总阀门”。管道再粗,没阀门控制流量,照样溢出。
在 @Cacheable 回源阶段嵌入 Sentinel 降级逻辑
Spring 的 @Cacheable 默认不感知下游异常,一旦数据库报错(如 SQLTimeoutException),它会把 null 或异常对象缓存起来,后续请求全走错误路径。必须打破这个默认行为:
- 禁用
@Cacheable的自动异常缓存:配置cacheManager.setCacheNullValues(false),避免 null 值污染缓存 - 用
@SentinelResource包裹实际的数据加载方法(如loadUserFromDb(Long id)),并在blockHandler中返回兜底数据(如空对象、静态默认值) - 确保
blockHandler不再触发缓存写入——否则会把兜底值也塞进 Redis,掩盖真实问题
示例关键片段:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
@SentinelResource(value = "user:load", blockHandler = "loadUserFallback")
public User loadUserFromDb(Long id) {
return userRepository.selectById(id); // 可能抛 SQLException
}
public User loadUserFallback(Long id, BlockException ex) {
log.warn("user {} load failed, use fallback", id);
return new User().setId(id).setName("unknown"); // 不调用 redisTemplate.opsForValue().set(...)
}
用 Sentinel 的系统规则 + 热点参数限流守住 Redis 和 DB 双入口
单纯对 Controller 接口限流不够——缓存穿透/击穿时,大量请求会绕过网关直击 Service 层的缓存加载逻辑。必须在更靠近数据源的位置设防:
- 对 Redis 操作做 QPS 限流:监控
redisTemplate.opsForValue().get()和.set()的调用频次,超过阈值时快速失败,防止 Redis 连接池打满 - 对数据库查询方法启用热点参数限流:比如
loadUserFromDb(Long id),设置参数id的 QPS 上限(如 100/s),避免某个恶意或异常id(如 -1、超大数)反复触发回源 - 开启 Sentinel 系统规则中的
load(CPU 使用率)和rt(平均响应时间)自适应保护:当 DB 响应变慢或机器负载飙升时,自动降级非核心接口,保主链路
注意:@SentinelResource 对 Spring Cache 的 AOP 代理无效,必须作用于具体的数据加载方法,而非 @Cacheable 所在的 public 方法。
本地缓存(Caffeine)+ Sentinel 兜底构成三层防御
从快到慢、从近到远,防御要分层:
- L1:Caffeine 本地缓存 —— 命中即返回,不走网络,扛住 80%+ 热点读请求;需配置
expireAfterWrite(2, TimeUnit.MINUTES),避免本地缓存长期不更新 - L2:Redis 分布式缓存 —— Caffeine 未命中时查询;配合
setex(key, 300 + rand.Intn(120), value)防集中过期 - L3:Sentinel
blockHandler+ 静态兜底数据 —— Redis 和 DB 全挂时的最后一道防线,不抛异常、不重试、不写缓存,直接返回预设安全值
最容易被忽略的是第三层:很多人以为 Sentinel 只管“限流”,其实它的 blockHandler 是唯一能在缓存和 DB 同时失效时,阻止线程阻塞、避免雪崩蔓延的实时干预点。没配它,等于没装刹车。

















