缓存雪崩解决案例

wen java案例 1

本文目录导读:

缓存雪崩解决案例

  1. 案例背景(业务场景)
  2. 事故原因分析
  3. 核心解决方案(代码 + 架构级)
  4. 优化后的解决方案总览(架构图)
  5. 解决后的效果
  6. 技术要点总结(面试/方案汇报可用)

下面是一个缓存雪崩的完整解决案例,包含问题背景、分析、核心解决方案(代码实现)以及优化后的架构图,方便你直接用于技术方案设计或面试讲解。


案例背景(业务场景)

业务场景:某电商平台首页,为了提升性能,将首页商品列表、轮播图、广告位等数据缓存在 Redis 中(缓存时间为固定 30 分钟)。 现象:某日凌晨 2 点,数据库突然遭受每秒上万次的查询请求,CPU 瞬间飙升至 100%,导致数据库连接池耗尽,服务不可用(宕机)。


事故原因分析

排查发现,凌晨 2 点是批处理任务统一更新数据的时间,所有缓存键都设置了相同的过期时间(30分钟)。 由于大量缓存同时过期(或 Redis 实例在某一刻重启崩溃),大量请求同时穿透缓存直达数据库,这就是典型的缓存雪崩


核心解决方案(代码 + 架构级)

针对上述原因,我们采用了四种方案叠加,彻底解决了问题。

方案 1:过期时间加随机数(治标——防止同时失效)

核心思想:让缓存过期时间不是一个固定值,而是基础时间 + 随机值,这样缓存不会在同一秒集体失效。 代码示例(Java + Spring Boot)

@Service
public class HomePageService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private ProductMapper productMapper;
    // 缓存键前缀
    private static final String PRODUCT_KEY = "product:home:";
    public List<Product> getHomeProducts() {
        String key = PRODUCT_KEY + "list";
        // 1. 从缓存读取
        String json = redisTemplate.opsForValue().get(key);
        if (StringUtils.hasText(json)) {
            return JSON.parseArray(json, Product.class);
        }
        // 2. 缓存失效,加锁(防止并发击穿)
        // 此处使用分布式锁,下文会提到,这里只展示缓存重建策略
        List<Product> products = productMapper.getHomeList();
        // 3. 【关键】设置缓存过期时间:基础 30 分钟 + 0~300 秒的随机数
        int baseTime = 30 * 60; // 30分钟
        int randomTime = new Random().nextInt(5 * 60); // 0 - 5分钟随机
        redisTemplate.opsForValue().set(key, JSON.toJSONString(products), 
                                        baseTime + randomTime, TimeUnit.SECONDS);
        return products;
    }
}

方案 2:互斥锁(分布式锁——防止雪崩瞬间打垮数据库)

核心思想:如果缓存没命中,只有一个线程能去数据库查询并重建缓存,其他线程等待一段时间后重试,这能防止雪崩时的请求全部打到数据库。 代码示例(Redisson 分布式锁)

public List<Product> getHomeProductsWithLock() {
    String key = "product:home:list";
    String lockKey = "lock:" + key;
    // 1. 先查缓存
    String json = redisTemplate.opsForValue().get(key);
    if (StringUtils.hasText(json)) {
        return JSON.parseArray(json, Product.class);
    }
    // 2. 缓存不存在,获取分布式锁(只允许一个线程去查库)
    RLock lock = redissonClient.getLock(lockKey);
    List<Product> products = null;
    try {
        // 尝试加锁,等待 3 秒,锁 10 秒自动释放
        if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
            // 双重校验(防止在等待锁期间,其他线程已经重建了缓存)
            json = redisTemplate.opsForValue().get(key);
            if (StringUtils.hasText(json)) {
                return JSON.parseArray(json, Product.class);
            }
            // 真正去数据库查询
            products = productMapper.getHomeList();
            // 设置缓存过期时间(带随机数)
            redisTemplate.opsForValue().set(key, JSON.toJSONString(products), 
                                            30*60 + new Random().nextInt(300), TimeUnit.SECONDS);
        } else {
            // 没拿到锁,说明其他线程在重建缓存,睡 50ms 后重试(递归调用自己)
            Thread.sleep(50);
            return getHomeProductsWithLock(); 
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
    return products;
}

方案 3:Redis 集群高可用(治本——防止 Redis 宕机)

解决方案:放弃单机 Redis,改为主从 + 哨兵模式,或者直接使用 Redis Cluster 或云厂商的 Redis 服务

  • 目的:即使一台 Redis 节点宕机,系统会自动切换到从节点或者通过分片机制保证缓存服务持续可用。

方案 4:服务熔断与降级(防线——兜底方案)

核心思想:万一数据库真的扛不住或者网络异常,主动降级,返回默认的兜底数据(如空列表或固定推荐商品),而不是让请求直接穿透到数据库。

public List<Product> getHomeProductsFallback() {
    try {
        // 正常查询缓存,超时时间设为 200ms
        String json = redisTemplate.opsForValue().get("product:home:list", 200, TimeUnit.MILLISECONDS);
        if (StringUtils.hasText(json)) {
            return JSON.parseArray(json, Product.class);
        }
    } catch (Exception e) {
        log.error("Redis 查询异常,触发降级逻辑");
    }
    // 数据库可用时,去查数据库;数据库也不可用时,返回兜底数据
    try {
        return productMapper.getHomeList();
    } catch (Exception dbException) {
        log.error("DB异常,返回兜底数据");
        // 返回预设的静态热门商品列表(放在类变量中)
        return DEFAULT_HOT_PRODUCTS;
    }
}

优化后的解决方案总览(架构图)

结合以上方案,最终的流程如下:

用户请求
   │
   ▼
【Controller】
   │
   ▼
【缓存查询层】
   ├── 1. 查询 Redis (超时时间 200ms)
   │
   ├── 命中? ──── Yes ────► 直接返回(走缓存,不查DB)
   │
   └── Miss (未命中)
          │
          ▼
   【互斥锁 + 二级缓存(本地缓存)】
          │  (只有拿到锁的线程进入)
          ▼
     查询 MySQL
          │
          ▼
     写入 Redis(并设置【基础时间+随机数】过期)
          │
          ▼
     返回结果
【边缘防护】(保障数据源安全)
   ├── Redis Cluster (高可用,避免单点故障)
   ├── 数据库连接池限流(如 HikariCP 设置最大连接数)
   └── Sentinel 熔断降级(当错误率达到阈值,直接返回兜底数据)

解决后的效果

  • 性能:数据库 QPS 从故障时的 10000+ 降为 0(正常时 <100),缓存命中率恢复至 99.9%。
  • 稳定性:即使批量任务更新导致缓存数据刷新,由于过期时间分散,数据库永远只承受最低限度的查询压力
  • 容灾能力:即使 Redis 集群部分宕机,由于有本地缓存和兜底数据,用户在页面上依然能看到内容,体验无感知。

技术要点总结(面试/方案汇报可用)

  1. 散列过期时间(加随机数)是所有方案中成本最低、见效最快的。
  2. 互斥锁 是保证数据一致性和防止数据库瞬间被打垮的关键。
  3. 高可用集群 是底线,防止硬件层面的灾难。
  4. 降级策略 是最后一道防线,保证用户体验不中断。

抱歉,评论功能暂时关闭!