Redis 缓存穿透雪崩和击穿

在使用 Redis 缓存时,常见的问题包括缓存穿透、缓存雪崩和缓存击穿,以下分别从问题表现、可能原因及解决方法进行阐述:

1. 缓存穿透

  • 问题表现:客户端请求的数据在缓存和数据库中都不存在,每次请求都会穿透缓存直接查询数据库,导致数据库压力增大,甚至可能引发数据库宕机。
  • 可能原因
    • 恶意攻击,如黑客故意请求不存在的数据,频繁访问数据库。
    • 业务数据存在异常,某些数据在正常情况下不会存在于数据库中,但被误请求。
  • 解决方法
    • 布隆过滤器(Bloom Filter):在缓存之前使用布隆过滤器,布隆过滤器可以快速判断某个数据是否存在。如果布隆过滤器判断数据不存在,就直接返回,不再查询数据库。布隆过滤器存在一定的误判率,但可以通过调整参数来降低误判率。例如在 Java 中,可以使用 Google Guava 库中的 BloomFilter
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

// 创建布隆过滤器,预计元素数量为10000,误判率为0.01
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 10000, 0.01);
// 添加元素
bloomFilter.put(1);
// 判断元素是否存在
boolean mightContain = bloomFilter.mightContain(1);
- **缓存空值**:当查询数据库发现数据不存在时,也将空值缓存起来,并设置一个较短的过期时间,这样下次相同请求就可以直接从缓存获取空值,而不会穿透到数据库。
// 假设jedis是Jedis实例
String key = "nonexistent_key";
String valueFromDb = getValueFromDatabase(key);
if (valueFromDb == null) {
    // 缓存空值,过期时间设为1分钟
    jedis.setex(key, 60, "");
    return "";
} else {
    // 缓存正常数据
    jedis.setex(key, 3600, valueFromDb);
    return valueFromDb;
}

2. 缓存雪崩

  • 问题表现:大量的缓存数据在同一时间过期,导致这些过期数据的请求同时穿透到数据库,使数据库瞬间承受巨大压力,甚至可能导致数据库崩溃。
  • 可能原因
    • 缓存数据设置了相同的过期时间,比如在系统初始化时,将大量数据的缓存过期时间都设置为一天,一天后这些缓存同时过期。
    • 缓存服务器重启或故障,导致所有缓存数据丢失,重启后大量请求直接访问数据库。
  • 解决方法
    • 随机过期时间:在设置缓存过期时间时,采用一个随机的时间范围,避免大量缓存同时过期。例如原本过期时间设置为 1 小时,可以改为在 50 分钟到 70 分钟之间随机设置过期时间。
import java.util.Random;
// 假设jedis是Jedis实例
String key = "example_key";
String value = getValueFromDatabase(key);
// 随机过期时间在50到70分钟之间
Random random = new Random();
int expireTime = 50 + random.nextInt(21);
jedis.setex(key, expireTime * 60, value);
- **二级缓存**:使用两层缓存,第一层缓存失效后,先从第二层缓存获取数据。第二层缓存可以设置较长的过期时间或者不设置过期时间。例如,可以使用 Caffeine 作为本地缓存作为二级缓存。
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

// 创建Caffeine缓存
Cache<String, String> caffeineCache = Caffeine.newBuilder()
      .build();

// 假设jedis是Jedis实例
String key = "example_key";
String valueFromRedis = jedis.get(key);
if (valueFromRedis == null) {
    String valueFromCaffeine = caffeineCache.getIfPresent(key);
    if (valueFromCaffeine != null) {
        return valueFromCaffeine;
    } else {
        String valueFromDb = getValueFromDatabase(key);
        if (valueFromDb != null) {
            jedis.setex(key, 3600, valueFromDb);
            caffeineCache.put(key, valueFromDb);
            return valueFromDb;
        }
    }
}
return valueFromRedis;
- **缓存预热**:在系统上线前,提前将部分热点数据加载到缓存中,并设置不同的过期时间,避免上线后大量数据同时过期。

3. 缓存击穿

  • 问题表现:一个热点数据在缓存过期的瞬间,大量并发请求同时访问该数据,这些请求会绕过缓存直接访问数据库,导致数据库压力瞬间增大。
  • 可能原因
    • 热点数据的过期时间设置不合理,或者由于某些原因,热点数据的缓存突然失效。
    • 高并发场景下,对热点数据的访问频率极高,缓存过期时大量请求同时到达。
  • 解决方法
    • 互斥锁:在缓存过期时,使用互斥锁(如 Redis 的 SETNX 命令)保证只有一个请求能去查询数据库并更新缓存,其他请求等待。获取到锁的请求查询数据库并更新缓存后,释放锁,其他请求就可以从缓存中获取数据。
import redis.clients.jedis.Jedis;

// 假设jedis是Jedis实例
String key = "hot_key";
String mutexKey = "mutex:" + key;
String value = jedis.get(key);
if (value == null) {
    // 使用SETNX获取互斥锁
    if (jedis.setnx(mutexKey, "1") == 1) {
        try {
            value = getValueFromDatabase(key);
            if (value != null) {
                jedis.setex(key, 3600, value);
            }
        } finally {
            // 释放互斥锁
            jedis.del(mutexKey);
        }
    } else {
        // 未获取到锁,等待一段时间后重试
        Thread.sleep(100);
        return getValue(key);
    }
}
return value;
- **永不过期**:对于热点数据不设置过期时间,通过后台线程定时更新缓存数据,或者在数据发生变化时主动更新缓存。这样可以避免因缓存过期导致的缓存击穿问题。
// 后台线程定时更新缓存示例
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import redis.clients.jedis.Jedis;

public class CacheUpdater {
    private static final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
    private static final String key = "hot_key";
    private static final Jedis jedis = new Jedis("localhost");

    static {
        executorService.scheduleAtFixedRate(() -> {
            String value = getValueFromDatabase(key);
            if (value != null) {
                jedis.set(key, value);
            }
        }, 0, 60, TimeUnit.MINUTES);
    }

    public static String getValue(String key) {
        return jedis.get(key);
    }

    private static String getValueFromDatabase(String key) {
        // 实际从数据库查询逻辑
        return "data from db";
    }
}
#牛客创作赏金赛##找工作如何保持松弛感?#
全部评论

相关推荐

评论
1
2
分享

创作者周榜

更多
牛客网
牛客企业服务