[redis]缓存击穿, 缓存穿透,缓存雪崩(介绍)

12 阅读4分钟

一、概念解释

1. 缓存击穿(Cache Breakdown)

  • 定义:某个热点key在缓存过期(失效)的瞬间,大量请求同时访问这个数据,导致所有请求直接穿透到数据库

  • 特点:针对单个热点key,在过期瞬间并发访问量大

  • 示例:热门商品详情页缓存过期时,大量用户同时点击

  • 关键词: 热点key, 过期瞬间, 大量访问

  • 总结: 热点key过期的瞬间, 这个时候再去从数据库读取, 然后再redis进行缓存, 已经来不及了, 因为是热点的key, 所以在那一瞬间, 高并发访问, 最终导致缓存击穿.

2. 缓存穿透(Cache Penetration)

  • 定义:查询一个数据库中根本不存在的数据,缓存无法命中,每次请求都直接访问数据库(数据库中没有, 那么redis中肯定也没有)
  • 特点:查询不存在的数据,可能是恶意攻击或业务逻辑问题
  • 示例:攻击者用不存在的用户ID频繁查询用户信息
  • 关键词: 查询压根就不存在的key

3. 缓存雪崩(Cache Avalanche)

  • 定义大量缓存数据在同一时间过期或Redis服务宕机,导致所有请求直接访问数据库
  • 特点:影响范围广,多个key同时失效
  • 示例:批量设置的缓存设置了相同的过期时间,到期时全部失效

二、严重程度比较

缓存雪崩 > 缓存击穿 > 缓存穿透

理由

  • 缓存雪崩:影响范围最广,可能直接导致数据库崩溃,系统整体不可用
  • 缓存击穿:只影响单个热点key,但可能引发连锁反应
  • 缓存穿透:通常只影响特定不存在的key,影响相对有限

三、解决方案

缓存击穿的解决方案

// 1. 使用互斥锁(分布式锁)
public Object getData(String key) {
    Object data = redis.get(key);
    if (data == null) {
        // 获取分布式锁
        if (lock.tryLock()) {
            try {
                // 双重检查
                data = redis.get(key);
                if (data == null) {
                    data = db.query(key);  // 查询数据库
                    redis.setex(key, 3600, data);  // 写入缓存
                }
            } finally {
                lock.unlock();
            }
        } else {
            // 等待其他线程加载数据
            Thread.sleep(100);
            return getData(key);  // 重试
        }
    }
    return data;
}

// 2. 设置热点数据永不过期 + 后台异步更新
redis.set(key, data);  // 不设置过期时间

// 3. 逻辑过期时间
redis.set(key, {
    "data": actualData,
    "expire_time": System.currentTimeMillis() + 3600000  // 逻辑过期时间
});

缓存穿透的解决方案

// 1. 缓存空对象
public Object getData(String key) {
    Object data = redis.get(key);
    if (data != null) {
        // 如果是空对象标记
        if (EMPTY_OBJECT.equals(data)) {
            return null;
        }
        return data;
    }
    
    data = db.query(key);
    if (data == null) {
        // 缓存空值,设置较短过期时间
        redis.setex(key, 300, EMPTY_OBJECT);
        return null;
    }
    
    redis.setex(key, 3600, data);
    return data;
}

// 2. 布隆过滤器(推荐)
// 初始化布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(
    Funnels.stringFunnel(Charset.defaultCharset()),
    1000000,  // 预期数据量
    0.01      // 误判率
);

// 查询前先检查
public Object getData(String key) {
    if (!bloomFilter.mightContain(key)) {
        return null;  // 肯定不存在
    }
    // ... 正常查询流程
}

缓存雪崩的解决方案

// 1. 设置随机过期时间
public void setCache(String key, Object value) {
    // 基础过期时间 + 随机时间(0-300秒)
    int expireTime = 3600 + new Random().nextInt(300);
    redis.setex(key, expireTime, value);
}

// 2. 缓存预热
// 系统启动时或定时任务预加载热点数据
public void cacheWarmUp() {
    List<HotItem> hotItems = db.getHotItems();
    for (HotItem item : hotItems) {
        setCache("item:" + item.getId(), item);
    }
}

// 3. 构建高可用Redis集群
// 使用哨兵模式或集群模式,避免单点故障

四、综合解决方案对比

方案适用场景优点缺点
互斥锁缓存击穿保证一致性,避免重复查询降低吞吐量,实现复杂
布隆过滤器缓存穿透内存占用小,效率高有误判率,不支持删除
缓存空对象缓存穿透实现简单可能存储大量无效key
随机过期时间缓存雪崩简单有效不能完全避免雪崩
热点数据永不过期缓存击穿/雪崩完全避免过期问题需要异步更新,可能数据不一致
多级缓存所有场景提供更好的可用性架构复杂,一致性维护难

五、最佳实践建议

  1. 组合使用策略

    • 热点数据:永不过期 + 异步更新
    • 普通数据:随机过期时间 + 互斥锁
    • 查询接口:布隆过滤器 + 参数校验
  2. 监控告警

    // 监控缓存命中率
    double hitRate = cacheHitCount / (cacheHitCount + cacheMissCount);
    if (hitRate < 0.8) {  // 命中率低于80%告警
        alert("缓存命中率过低");
    }
    
  3. 降级方案

    // 数据库压力过大时降级
    if (dbConnections > threshold) {
        // 返回默认数据或排队等待
        return defaultData;
    }
    
  4. 使用现代解决方案

    • Redis 4.0+:使用LFU淘汰策略
    • Redis 6.0+:使用客户端缓存
    • 考虑使用Caffeine等本地缓存作为二级缓存

实际应用中,需要根据业务场景选择合适的组合方案,通常建议布隆过滤器防穿透 + 互斥锁防击穿 + 随机过期时间防雪崩的组合策略。