Redis缓存穿透:原理、危害与解决方案

壬炎V8
发布于 2025-4-10 12:50
浏览
0收藏

什么是缓存穿透?
缓存穿透是指查询一个数据库中根本不存在的数据,导致每次请求都会穿过缓存直接访问数据库的现象。与缓存击穿(某个热点key失效时大量请求直接打到数据库)不同,缓存穿透是针对不存在的数据的持续高并发查询。

缓存穿透的危害
数据库压力剧增:大量请求直接穿透缓存层到达数据库

系统性能下降:数据库负载过高导致整体响应变慢

潜在服务崩溃风险:极端情况下可能导致数据库崩溃

资源浪费:无效查询消耗系统资源

缓存穿透的常见场景
恶意GJ:故意查询不存在的ID或参数

业务逻辑缺陷:未对参数做有效性校验

数据淘汰:已删除数据的持续查询

爬虫请求:爬取不存在的页面或数据

解决方案

  1. 缓存空对象
public Object getData(String key) {
    // 1. 从缓存查询
    Object value = redis.get(key);
    if (value != null) {
        return value;
    }
    
    // 2. 缓存不存在,查询数据库
    value = database.get(key);
    if (value == null) {
        // 数据库也不存在,缓存空值并设置较短过期时间
        redis.setex(key, 60, "NULL"); // 60秒过期
        return null;
    }
    
    // 3. 数据库存在,写入缓存
    redis.setex(key, 3600, value); // 1小时过期
    return value;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

优点:实现简单,能有效阻挡重复的无效查询
缺点:可能缓存大量无用的空键,占用内存空间

  1. 布隆过滤器(Bloom Filter)
// 初始化布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(
    Funnels.stringFunnel(Charset.defaultCharset()), 
    1000000, // 预期元素数量
    0.01);   // 误判率

// 预热布隆过滤器
List<String> allKeys = database.getAllKeys();
for (String key : allKeys) {
    bloomFilter.put(key);
}

public Object getData(String key) {
    // 1. 先检查布隆过滤器
    if (!bloomFilter.mightContain(key)) {
        return null; // 肯定不存在
    }
    
    // 2. 布隆过滤器认为可能存在,继续正常流程
    Object value = redis.get(key);
    if (value != null) {
        return value;
    }
    
    value = database.get(key);
    if (value != null) {
        redis.setex(key, 3600, value);
    }
    return value;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

优点:内存效率高,能有效拦截绝对不存在的键
缺点:存在误判可能,需要预热,数据更新时需要同步更新过滤器

  1. 接口层校验
    参数格式校验(如ID必须为数字)

范围校验(如ID必须大于0)

业务规则校验(如手机号格式)

权限校验(如用户是否有权访问)

  1. 互斥锁方案
public Object getData(String key) {
    Object value = redis.get(key);
    if (value != null) {
        return value;
    }
    
    // 尝试获取分布式锁
    String lockKey = "lock:" + key;
    boolean locked = redis.setnx(lockKey, "1", 10); // 10秒超时
    if (!locked) {
        // 获取锁失败,短暂等待后重试
        Thread.sleep(100);
        return getData(key);
    }
    
    try {
        // 再次检查缓存,防止其他线程已经写入
        value = redis.get(key);
        if (value != null) {
            return value;
        }
        
        value = database.get(key);
        if (value == null) {
            // 缓存空对象
            redis.setex(key, 60, "NULL");
        } else {
            redis.setex(key, 3600, value);
        }
        return value;
    } finally {
        // 释放锁
        redis.del(lockKey);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.

优点:避免大量并发请求同时穿透到数据库
缺点:实现复杂,可能增加系统延迟

最佳实践建议
组合使用多种方案:例如布隆过滤器+空对象缓存

监控与告警:监控缓存命中率,设置合理阈值

合理设置空值过期时间:通常比正常缓存短

热点数据特殊处理:对特别频繁的查询做特殊优化

定期清理无效空缓存:避免内存浪费

总结
缓存穿透是分布式系统中常见的高并发问题,通过合理的技术组合和预防措施,可以有效地减轻甚至消除其影响。在实际应用中,应根据业务特点选择最适合的方案或组合方案,并持续监控系统表现,不断优化防护策略。

标签
已于2025-4-10 12:51:43修改
收藏
回复
举报
回复
    相关推荐