Redis雪崩把我坑惨了,这是5年经验的教训

"凌晨3点,整个集群的QPS从2万暴跌到200,值班手机被打爆------就因为一个简单的expire设置。"

去年双十一大促前,我们给核心商品页的缓存加了TTL。本以为只是个小优化,没想到压测时Redis集群突然全瘫,数据库连接池被打满。事后复盘才发现:所有Key在同一秒失效 ,请求像雪崩一样砸向数据库。

今天我就拆解这次事故,说说Redis雪崩怎么防、怎么治。

1. 雪崩是怎么发生的?

我们的场景很典型:商品详情页缓存2万SKU,TTL统一设了10分钟。听起来没问题对吧?但压测时用redis-cli --bigkeys一看,发现所有Key的expire时间戳完全一致!

  • 根因在RDB持久化 *:Redis默认的RDB快照会清空过期时间,重启后所有Key会同时加载 。如果你像我们一样用EXPIRE key 600批量设置TTL,所有Key的死亡时间就被对齐了!

错误代码长这样:

java 复制代码
// 批量设置缓存+过期时间(错误示范)
public void loadProducts(List<Product> products) {
    try (Jedis jedis = jedisPool.getResource()) {
        Pipeline pipeline = jedis.pipelined();
        products.forEach(p -> {
            pipeline.set("product:" + p.getId(), serialize(p));
            pipeline.expire("product:" + p.getId(), 600); // 所有Key会在同一秒失效
        });
        pipeline.sync();
    }
}

2. 解法:时间戳要加随机抖动

正确做法是在TTL上加随机值。比如600 ± 60秒,让Key的过期时间均匀分布:

java 复制代码
// 正确的随机过期时间设置
private int getRandomTtl() {
    return 600 + ThreadLocalRandom.current().nextInt(-60, 61); // 540~660秒随机
}

public void loadProductsSafe(List<Product> products) {
    try (Jedis jedis = jedisPool.getResource()) {
        Pipeline pipeline = jedis.pipelined();
        products.forEach(p -> {
            pipeline.set("product:" + p.getId(), serialize(p));
            pipeline.expire("product:" + p.getId(), getRandomTtl()); // 关键点
        });
        pipeline.sync();
    }
}

实测对比:

方案 缓存失效时的DB QPS峰值 恢复时间
固定TTL 12,000 >5分钟
随机TTL 2,300 <30秒

3. 深层优化:多级缓存与热Key续命

光改TTL还不够。我们后来补充了两个措施:

  1. 本地缓存兜底:用Caffeine做一级缓存,过期时间比Redis短(比如3分钟+随机),扛住Redis瞬时失效
  2. 异步热Key续期:通过定时任务扫描,提前续期高频访问的Key
java 复制代码
// 热Key续期示例(简化版)
@Scheduled(fixedDelay = 300_000) // 每5分钟执行
public void extendHotKeys() {
    hotKeyTracker.getTopKeys(100).forEach(key -> {
        jedis.expire(key, getRandomTtl()); // 续期时也要加随机
    });
}

4. 避坑清单:雪崩防御三板斧

  1. 永远不用固定TTL批量设置过期时间:这是最隐蔽的坑,RDB持久化会抹平时间差
  2. 监控expired_keys突增 :redis-cli info里的这个指标突然上涨就是前兆
  3. 慎用keys *命令扫全库 :大Key扫描用scan增量式迭代,否则可能引发连锁反应

5. 终极防御:熔断与降级

当雪崩真的发生时,快速失败比雪崩蔓延更重要:

java 复制代码
// 基于Hystrix的降级示例
@HystrixCommand(fallbackMethod = "getProductFallback")
public Product getProduct(String id) {
    String key = "product:" + id;
    String json = jedis.get(key);
    if (json == null) {
        // 这里可以加互斥锁防击穿,但篇幅限制略过
        return db.getProduct(id); 
    }
    return deserialize(json);
}

private Product getProductFallback(String id) {
    return defaultProduct; // 返回兜底数据
}

写在最后

Redis雪崩的可怕之处在于:它总在你最忙的时候来一把大的。经过这次教训,我们现在对所有批量操作TTL的代码都会Code Review三次------因为人永远会在同一个地方跌倒两次。

你的项目里是怎么预防雪崩的?有没有更骚的操作?评论区见。

相关推荐
量化分析码农15 分钟前
【Python量化多因子实战 06】从因子到持仓:多因子选股实盘工程化落地
后端
SameX21 分钟前
HarmonyOS ArkUI List 的侧滑删除 用 ListItem 的 swipeAction + endIcon 的做法
前端
心祭丫丫21 分钟前
【每天一个 Python 小案例17】Python 实现通讯录
开发语言·人工智能·笔记·python·学习
贾公子39 分钟前
检测链路的容量规划——用 Little's Law 算清 QPS、并发与积压
后端·python
kyson_44 分钟前
Java 集合框架:从继承体系到数据结构,再到开发选型
后端
海螺_AIHCC44 分钟前
LLM 返回的 JSON 十次坏三次,别急着骂模型——60 行容错解析器先兜住(附可运行代码)
人工智能
露天赏雪1 小时前
第3篇:Sentinel熔断降级实战:大模型接口抖动时的服务兜底方案
java·开发语言·人工智能·spring boot·spring·servlet·sentinel
lisw051 小时前
理解推理大语言模型的可监控性!
人工智能·语言模型·自然语言处理
天天被压力1 小时前
【A股分钟级高频数据完全手册 #06】北交所分钟级数据接口:京市股票Pro版最新分时
java·人工智能·python