Redis 缓存穿透雪崩和击穿
在使用 Redis 缓存时,常见的问题包括缓存穿透、缓存雪崩和缓存击穿,以下分别从问题表现、可能原因及解决方法进行阐述:
1. 缓存穿透
- 问题表现:客户端请求的数据在缓存和数据库中都不存在,每次请求都会穿透缓存直接查询数据库,导致数据库压力增大,甚至可能引发数据库宕机。
- 可能原因:
- 恶意攻击,如黑客故意请求不存在的数据,频繁访问数据库。
- 业务数据存在异常,某些数据在正常情况下不会存在于数据库中,但被误请求。
- 解决方法:
- 布隆过滤器(Bloom Filter):在缓存之前使用布隆过滤器,布隆过滤器可以快速判断某个数据是否存在。如果布隆过滤器判断数据不存在,就直接返回,不再查询数据库。布隆过滤器存在一定的误判率,但可以通过调整参数来降低误判率。例如在 Java 中,可以使用 Google Guava 库中的
BloomFilter
。
- 布隆过滤器(Bloom Filter):在缓存之前使用布隆过滤器,布隆过滤器可以快速判断某个数据是否存在。如果布隆过滤器判断数据不存在,就直接返回,不再查询数据库。布隆过滤器存在一定的误判率,但可以通过调整参数来降低误判率。例如在 Java 中,可以使用 Google Guava 库中的
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";
}
}
#牛客创作赏金赛##找工作如何保持松弛感?#