缓存穿透、缓存击穿和缓存雪崩

80 阅读3分钟

1. 缓存穿透

问题描述
缓存穿透是指查询一个在缓存和数据库中都不存在的数据。由于缓存不命中,每次请求都会直接访问数据库,导致数据库压力过大。

解决方案

  1. 缓存空对象

    • 当缓存和数据库中都没有查询到数据时,将一个空对象(如 null 或特殊标记)放入缓存,并设置一个较短的过期时间。
    • 这样,下一次查询相同的 key 时,会直接命中缓存,避免访问数据库。
    public Object getFromCache(String key) {
        Object value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            return value;
        }
        value = databaseQuery(key);
        if (value == null) {
            redisTemplate.opsForValue().set(key, "null", 60, TimeUnit.SECONDS);
        } else {
            redisTemplate.opsForValue().set(key, value, 3600, TimeUnit.SECONDS);
        }
        return value;
    }
    
  2. 布隆过滤器

    • 使用布隆过滤器在缓存之前快速判断一个 key 是否存在。
    • 布隆过滤器可以高效地判断一个元素是否可能存在于集合中,从而过滤掉大部分无效请求。
    // 初始化布隆过滤器
    BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 100000);
    
    public Object getFromCacheWithBloomFilter(String key) {
        if (!bloomFilter.mightContain(key)) {
            return null;
        }
        Object value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            return value;
        }
        value = databaseQuery(key);
        if (value == null) {
            redisTemplate.opsForValue().set(key, "null", 60, TimeUnit.SECONDS);
        } else {
            redisTemplate.opsForValue().set(key, value, 3600, TimeUnit.SECONDS);
        }
        return value;
    }
    

    2. 缓存击穿

    问题描述
    缓存击穿是指一个热点数据在缓存过期的瞬间,有大量请求同时访问该数据,导致这些请求都直接访问数据库,造成数据库压力激增。

    解决方案

  3. 互斥锁

    • 在缓存失效时,通过加锁来控制只有一个线程可以查询数据库并更新缓存,其他线程等待缓存更新完成后再获取数据。
    public Object getFromCacheWithLock(String key) {
        Object value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            return value;
        }
        synchronized (this) {
            value = redisTemplate.opsForValue().get(key);
            if (value != null) {
                return value;
            }
            value = databaseQuery(key);
            redisTemplate.opsForValue().set(key, value, 3600, TimeUnit.SECONDS);
        }
        return value;
    }
    
  4. 提前预热

    • 对于已知的热点数据,可以在缓存过期前主动更新缓存,避免缓存失效导致的击穿。
    // 定时任务提前预热缓存
    @Scheduled(fixedRate = 3600000)
    public void refreshHotCache() {
        List<String> hotKeys = getHotKeys();
        for (String key : hotKeys) {
            Object value = databaseQuery(key);
            redisTemplate.opsForValue().set(key, value, 3600, TimeUnit.SECONDS);
        }
    }
    

    3. 缓存雪崩

    问题描述
    缓存雪崩是指在某一时刻大量缓存同时失效,导致大量请求直接访问数据库,造成数据库压力过大甚至崩溃。

    解决方案

  5. 缓存过期时间设置随机化

    • 为不同的缓存设置不同的过期时间,避免大量缓存同时失效。
    Random random = new Random();
    int randomExpireTime = 3600 + random.nextInt(600); // 3600s 到 4200s 之间
    redisTemplate.opsForValue().set(key, value, randomExpireTime, TimeUnit.SECONDS);
    
  6. 双缓存机制

    • 使用双缓存机制,在旧缓存即将过期时,提前生成新缓存,确保缓存数据的连续性。
    public Object getFromCacheWithDoubleCache(String key) {
        String oldKey = key + ":old";
        String newKey = key + ":new";
        Object value = redisTemplate.opsForValue().get(newKey);
        if (value != null) {
            return value;
        }
        value = redisTemplate.opsForValue().get(oldKey);
        if (value != null) {
            // 异步更新新缓存
            asyncUpdateNewCache(key);
            return value;
        }
        value = databaseQuery(key);
        redisTemplate.opsForValue().set(newKey, value, 3600, TimeUnit.SECONDS);
        redisTemplate.opsForValue().set(oldKey, value, 7200, TimeUnit.SECONDS);
        return value;
    }
    
    private void asyncUpdateNewCache(String key) {
        new Thread(() -> {
            Object value = databaseQuery(key);
            redisTemplate.opsForValue().set(key + ":new", value, 3600, TimeUnit.SECONDS);
        }).start();
    }
    
  7. 限流降级

    • 在缓存失效时,限流部分请求,或者直接返回默认值,保护数据库不被压垮。
    public Object getFromCacheWithRateLimit(String key) {
        if (rateLimiter.tryAcquire()) {
            Object value = redisTemplate.opsForValue().get(key);
            if (value != null) {
                return value;
            }
            value = databaseQuery(key);
            redisTemplate.opsForValue().set(key, value, 3600, TimeUnit.SECONDS);
            return value;
        } else {
            // 返回默认值或降级处理
            return getDefaultResponse();
        }
    }
    

    总结

  • 缓存穿透:使用缓存空对象和布隆过滤器来防止无效请求穿透缓存。
  • 缓存击穿:使用互斥锁和提前预热来避免热点缓存失效导致的击穿。
  • 缓存雪崩:通过随机化过期时间、双缓存机制和限流降级来防止大量缓存同时失效引发的雪崩。