@Cacheable注解本身不支持timeToLive等TTL字段,硬加会编译报错;真正生效的是RedisCacheConfiguration.entryTtl()全局配置,或通过自定义RedisCacheWriter在写入时注入随机偏移实现防雪崩。

@Cacheable 注解本身不支持随机 TTL,硬加会编译报错 —— 因为它压根没有 ttl、timeToLive 或任何类似字段。
为什么 @Cacheable 里写 timeToLive = 60 会直接失败
IDE 报 Unknown property 'timeToLive' 不是配置漏了,是 Spring Cache 抽象层 API 根本没定义这个字段。所有基于注解的缓存操作都走统一的 CacheOperation 解析逻辑,而该逻辑只识别 cacheNames、key、unless 等有限属性。
- 试图用 SpEL 表达式动态算 TTL(如
timeToLive = "#{T(java.time.Duration).ofMinutes(30).plusSeconds(T(java.util.concurrent.ThreadLocalRandom).current().nextInt(-180, 180)).getSeconds()}")也无效 —— 注解属性值必须是编译期常量 - Spring Boot 3.x + Spring Cache 6.x 依然维持该设计,未引入 per-key 动态 TTL 支持
- 真正起作用的是
RedisCacheConfiguration中的entryTtl,但它只是静态基础值,无法在每次put()时扰动
用自定义 RedisCacheWriter 在写入时注入随机偏移
这是最可控、侵入最小、且能复用现有 @Cacheable 代码的方式:不改业务逻辑,只接管底层写入行为。
- 继承
DefaultRedisCacheWriter,重写put()方法,在调用super.put()前对传入的timeout参数做 ±10% 随机浮动 - 使用
ThreadLocalRandom.current().nextInt(-offset, offset + 1),避免多线程下new Random()种子冲突 - 确保每个
RedisCacheManager显式通过cacheWriter()注入该实现;否则 fallback 到默认 writer,随机逻辑失效 - 若需按
cacheName差异化策略(如"user"加 ±30s,"config"不加),在put()内部加if (cacheName.equals("user")) { ... }路由判断
别依赖 redisTemplate.set(key, value, Duration) 替代方案
很多人想绕过 Spring Cache 直接用 RedisTemplate 手动设值,但这会破坏 @Cacheable 的语义一致性,导致以下问题:
- 缓存 key 生成规则不一致:
@Cacheable(key = "#id")默认用 SimpleKeyGenerator,而手写 key 可能拼错或漏前缀 - 无法自动触发
@CacheEvict、@CachePut等联动行为 - 丢失 cache name 分组能力,后续运维查
redis-cli --scan --pattern "cache:*"时无法按业务维度归类 - 如果用了
RedisCacheConfiguration的序列化器(如GenericJackson2JsonRedisSerializer),手动 set 时忘了配相同序列化器,会导致反序列化失败
Redis 端和应用层的双重随机才是真防雪崩
Java 层加随机只是第一道防线。Redis 自身的过期清理机制才是最终保障 —— 它靠“惰性删除 + 定期采样”,但默认每秒只扫 10 次,每次最多检查 20 个键。如果大量 key 的 EXPIREAT 时间戳被设成同一整点(比如全设为 1729436400),那一秒内 Redis 可能被过期扫描压垮。
- 务必在业务写入时用
redisTemplate.expire(key, randomSeconds, TimeUnit.SECONDS),而不是统一算好时间戳再批量EXPIREAT - 随机范围建议控制在基础 TTL 的 ±10% 以内(如基础 3600s,则 ±360s),偏离太大可能让热点数据未被访问就过期
- 本地缓存(如 Caffeine)要配合开启
recordStats(),监控命中率突降 —— 雪崩前兆往往不是 Redis 崩了,而是cache.getIfPresent()命中率从 95% 掉到 40%











