Java缓存雪崩案例怎么预防处理

wen java案例 24

本文目录导读:

Java缓存雪崩案例怎么预防处理

  1. 什么是缓存雪崩?核心原因是什么?
  2. 典型案例
  3. 预防措施(核心)
  4. 缓存节点宕机怎么办?
  5. 最佳实践组合

这是一个非常经典的分布式系统高并发问题,缓存雪崩发生时,大量请求直接打到数据库,极易导致数据库崩溃,进而引发整个系统不可用。

下面从原因分析预防措施事后处理三个层面,结合具体案例和Java代码实现,来拆解如何应对缓存雪崩。


什么是缓存雪崩?核心原因是什么?

定义:缓存中的数据在同一时刻大面积过期,导致大量请求无法命中缓存,直接涌入数据库,数据库瞬间压力过大甚至宕机。

核心原因

  1. 缓存数据同时失效:大量key设置了相同的过期时间(如统一设置为0点过期)。
  2. 缓存节点宕机:Redis集群中部分节点挂掉,导致部分热点key不可用。

典型案例

场景:电商APP首页的商品分类、推荐列表、热门活动等数据,缓存在Redis中,过期时间统一设为24小时(凌晨0点过)。

现象

  • 凌晨0点0分0秒,所有缓存同时失效。
  • 此时百万用户刷新首页 -> 所有请求穿透到MySQL -> 数据库连接池打满 -> 请求堆积 -> MySQL崩溃 -> 整个首页不可用。

预防措施(核心)

预防的思路是避免大量key同时失效保护数据库

设置随机过期时间(最基础且有效)

给缓存过期时间增加一个随机值,避免集中失效。

import redis.clients.jedis.Jedis;
import java.util.Random;
public class CacheService {
    private Jedis jedis = new Jedis("localhost");
    private Random random = new Random();
    private static final int BASE_EXPIRE = 3600; // 基础过期时间:1小时
    public void setRandomExpire(String key, String value) {
        // 基础过期时间 + 0~600秒的随机偏移
        int expireTime = BASE_EXPIRE + random.nextInt(600);
        jedis.setex(key, expireTime, value);
    }
}

优点:实现简单,适合大多数场景。

使用二级缓存(本地缓存 + 分布式缓存)

使用本地缓存(如Caffeine、Guava Cache)作为一级缓存,Redis作为二级缓存,本地缓存不集中失效,能有效拦截第一波流量。

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
public class MultiLevelCache {
    // 一级:本地缓存(过期时间短,且随机)
    private Cache<String, Object> localCache = Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.SECONDS) // 本地缓存10秒过期
            .maximumSize(10000)
            .build();
    // 二级:Redis
    private CacheService redisService = new CacheService();
    public Object get(String key) {
        // 1. 查本地缓存
        Object localValue = localCache.getIfPresent(key);
        if (localValue != null) {
            return localValue;
        }
        // 2. 查Redis
        Object redisValue = redisService.get(key);
        if (redisValue != null) {
            // 写入本地缓存
            localCache.put(key, redisValue);
            return redisValue;
        }
        // 3. 查数据库(加互斥锁保护)
        return loadFromDB(key);
    }
    private Object loadFromDB(String key) {
        // ... 数据库查询逻辑(建议配合分布式锁)
        return null;
    }
}

缓存永不过期 + 后台异步更新(热点数据利器)

只设置逻辑过期时间,物理上永不过期,后台线程定期检查并刷新缓存。

public class HotCacheManager {
    private Jedis jedis;
    private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5);
    public void setHotData(String key, String value, int logicalExpireSeconds) {
        // 存储数据 + 逻辑过期时间戳
        String cacheValue = value + "|" + (System.currentTimeMillis() + logicalExpireSeconds * 1000);
        jedis.set(key, cacheValue); // 物理上永不过期
        // 启动后台线程,逻辑过期前主动刷新(比如提前10秒)
        scheduler.schedule(() -> refreshCache(key), logicalExpireSeconds - 10, TimeUnit.SECONDS);
    }
    public String getHotData(String key) {
        String cacheValue = jedis.get(key);
        if (cacheValue == null) {
            return null;
        }
        // 解析逻辑过期时间
        String[] parts = cacheValue.split("\\|");
        long expireTime = Long.parseLong(parts[1]);
        // 如果逻辑过期,触发异步更新(但不阻塞当前请求)
        if (System.currentTimeMillis() > expireTime) {
            scheduler.submit(() -> refreshCache(key));
        }
        return parts[0]; // 返回旧数据
    }
    private void refreshCache(String key) {
        // 1. 加分布式锁,防止重复刷新
        // 2. 从数据库加载最新数据
        // 3. 更新缓存(重新设置逻辑过期时间)
        System.out.println("Refreshing cache for key: " + key);
    }
}

互斥锁(缓存重建时保护数据库)

当缓存失效时,只允许一个线程去查数据库,其他线程等待。

public class CacheWithMutex {
    private Jedis jedis;
    private static final String LOCK_KEY_PREFIX = "LOCK:";
    public String get(String key) {
        String value = jedis.get(key);
        if (value != null) {
            return value;
        }
        // 尝试获取分布式锁
        String lockKey = LOCK_KEY_PREFIX + key;
        String lockValue = UUID.randomUUID().toString();
        // SET NX EX 10:不存在才设置,过期10秒(防止死锁)
        String result = jedis.set(lockKey, lockValue, "NX", "EX", 10);
        if ("OK".equals(result)) {
            // 成功获取锁
            try {
                // 双重检查:可能其他线程已经重建了缓存
                value = jedis.get(key);
                if (value != null) {
                    return value;
                }
                // 查数据库
                value = queryDB(key);
                // 写入缓存
                jedis.setex(key, 3600, value);
                return value;
            } finally {
                // 释放锁(用Lua脚本保证原子性)
                String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                jedis.eval(luaScript, 1, lockKey, lockValue);
            }
        } else {
            // 未获取到锁,等待后重试
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return get(key); // 递归重试(注意可能栈溢出,建议用循环)
        }
    }
    private String queryDB(String key) {
        // 模拟数据库查询
        return "data_from_db";
    }
}

缓存节点宕机怎么办?

如果是因为Redis集群节点宕机,不是key同时过期。

Redis集群高可用

  • 主从架构 + Sentinel:自动故障转移。
  • Redis Cluster:数据分片,部分节点宕机不影响整体。

本地缓存兜底(更上一层保险)

即使Redis完全不可用,本地缓存还能扛住一段时间。

限流熔断(保护数据库的最后防线)

  • Sentinel(Hystrix/Sentinel限流组件):在数据库访问层设置限流阈值。
  • 当请求量超过阈值时,直接返回降级数据(如“系统繁忙,请稍后重试”)或默认数据。
// 使用Sentinel做限流示例(伪代码)
@SentinelResource(value = "queryDB", blockHandler = "blockHandler")
public String queryDB(String key) {
    // 实际数据库查询
    return "real_data";
}
public String blockHandler(String key, BlockException e) {
    // 降级返回默认值
    return "default_data";
}

最佳实践组合

针对缓存雪崩,推荐按以下优先级组合使用:

场景 方案 复杂度 效果
通用预防 设置随机过期时间 有效避免集中失效
热点key 永不过期 + 异步刷新 彻底解决过期问题
高安全要求 二级缓存(本地+Redis) 多一层缓冲
并发大时 互斥锁重建缓存 保护数据库
硬件故障 Redis高可用(Cluster/Sentinel) 应对宕机
极限保护 限流、降级、熔断 防止系统崩溃

日常开发建议

  1. 新项目:优先采用 随机过期时间 + 本地缓存 组合,简单可靠。
  2. 高并发核心链路:引入 后台异步刷新 + 互斥锁,保证缓存一直存在且数据最终一致。
  3. 最终防线:所有数据库访问层必须配置 限流熔断,避免缓存失效时系统被拖垮。

通过以上措施,可以有效将缓存雪崩的概率降到极低,即使发生,系统也能平稳降级,不会直接崩溃。

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