核心解法是为Redis String缓存的EX参数添加随机偏移量,避免批量key集中过期导致雪崩;需用ThreadLocalRandom在应用层控制±10%内合理随机范围,并配合本地缓存、空值随机TTL、监控告警等兜底措施。

Redis String 类型缓存雪崩,核心解法就是不让大量 SET 命令带相同 EX 参数批量写入——必须加随机偏移量。
为什么 set key value EX 3600 会引发雪崩
当业务批量初始化缓存(比如凌晨定时任务刷新商品信息),所有 String key 都用固定 TTL(如 3600 秒),它们会在同一时间点集中过期。下一秒流量涌来,全部击穿到 DB,数据库瞬间被打满。
这不是 Redis 的 bug,是人为设置的“定时炸弹”。EX 值越精确、越统一,风险越高。
- 典型场景:Spring Boot 启动时用
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(3600))批量预热 - 错误现象:
redis-cli --scan --pattern "product:*" | xargs -I{} redis-cli ttl {}查出大量 key 的 TTL 都是完全相同的整数 - 关键点:Redis 本身不提供“自动加随机”的机制,必须在应用层控制
如何给 String 缓存加随机过期时间(Java + RedisTemplate)
不能只靠 Random.nextInt() 简单叠加,要考虑分布合理性与可维护性。
- 基础 TTL 设为 3600 秒(1 小时),随机偏移建议控制在 ±10% 以内(即 ±360 秒),避免部分 key 过期太快或太慢
- 用
ThreadLocalRandom.current().nextInt(0, 360)替代全局Random,避免多线程竞争 - 务必封装成工具方法,禁止在业务代码里重复写随机逻辑
- 示例:
public void setWithRandomExpiry(String key, Object value, int baseSeconds, int randomRangeSeconds) { int actualSeconds = baseSeconds + ThreadLocalRandom.current().nextInt(0, randomRangeSeconds); redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(actualSeconds)); } - 如果用的是
Jedis,对应调用jedis.set(key, value, SetParams.setParams().ex(actualSeconds))
随机值不是越大越好,要避开几个坑
加随机不是为了“看起来分散”,而是让失效压力在时间轴上真正摊薄。偏离设计目标反而引入新问题。
- 随机范围超过基础 TTL 的 20%,会导致部分 key 寿命缩短太多(比如 3600s 基础 + -720s 偏移 → 实际只有 2880s),热点数据可能还没被访问就过期了
- 用
new Random().nextInt()在高并发下会产生相同种子,导致多个线程拿到一样的随机数——必须用ThreadLocalRandom - 不要对同一个 key 多次调用
setWithRandomExpiry覆盖,否则每次都会重置过期时间,失去“错峰”意义 - 如果用 Redis Cluster,注意客户端是否支持 pipeline 批量发送带不同 TTL 的
SET命令;某些老版本 Lettuce 在 pipeline 中无法为每个命令单独设EX
String 雪崩还有比随机更关键的兜底项
随机过期时间只是第一道防线。它防不住 Redis 整体宕机、网络分区、或误操作 FLUSHDB。实际线上必须组合其他手段:
- 本地缓存兜底:对极关键的 String 数据(如系统开关、降级配置),用
Caffeine或ConcurrentHashMap存一份,Redis 不可用时直接返回 - 空值也加随机 TTL:缓存穿透场景下,
SET product:999999 "" EX 120这种空值缓存同样要加随机,否则大量空 key 也会扎堆失效 - 监控告警必须覆盖:用
redis-cli info keyspace定期采样各 db 的expires数量变化率,突降 50% 就该触发告警 - 别忘了
SETNX不是万能的:缓存击穿用SETNX加锁重建时,若锁过期时间没和缓存 TTL 对齐,可能造成旧值残留或重复重建
真正容易被忽略的是:随机过期只解决“时间集中”,不解决“空间集中”。如果所有 key 都落在 Redis 同一个 slot(比如都用 product:{id} 且 id 是连续数字),主节点压力依然会倾斜。这时候得配合 key 命名优化或哈希扰动。











