47. 什么是Redis的缓存穿透?如何在Java中防止缓存穿透?
大约 4 分钟
什么是Redis的缓存穿透?
缓存穿透 是指在使用缓存系统(如 Redis)时,请求的数据在缓存中不存在,而且在数据库中也不存在,导致每次请求都要查询数据库。由于查询不到数据,每次查询结果都不会被缓存,进而导致所有请求都落到数据库上,造成数据库负载过重,甚至可能导致数据库崩溃。
例如,如果有大量请求查询一个数据库中不存在的键,由于缓存中没有命中,查询会直接穿透缓存,导致大量请求直接落到数据库。
如何在Java中防止缓存穿透?
在 Java 中,防止缓存穿透的常见方法包括以下几种:
1. 缓存空对象
当查询一个键时,如果数据库中不存在对应的数据,可以将这个“空结果”缓存到 Redis 中,设置一个短暂的过期时间。这样后续查询相同键时,就会直接从缓存中获取到“空结果”,从而避免每次都查询数据库。
实现步骤:
- 查询缓存,如果命中则返回结果。
- 如果缓存中不存在,查询数据库。
- 如果数据库中也不存在,将空结果(如
null
或""
)写入缓存,并设置一个合理的过期时间。 - 后续相同的查询会命中缓存中的空结果,避免穿透。
Java代码示例:
import redis.clients.jedis.Jedis;
public class CachePenetrationExample {
private Jedis jedis;
public CachePenetrationExample(Jedis jedis) {
this.jedis = jedis;
}
public String getData(String key) {
String value = jedis.get(key);
if (value != null) {
return "null".equals(value) ? null : value;
}
// 模拟数据库查询
value = queryFromDatabase(key);
if (value == null) {
jedis.setex(key, 60, "null"); // 缓存空结果,设置60秒过期时间
} else {
jedis.set(key, value); // 缓存真实数据
}
return value;
}
private String queryFromDatabase(String key) {
// 模拟数据库查询
// return null if not found
return null;
}
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
CachePenetrationExample cacheExample = new CachePenetrationExample(jedis);
String value = cacheExample.getData("nonexistent_key");
System.out.println("Value: " + value);
}
}
2. 使用布隆过滤器(Bloom Filter)
布隆过滤器是一种概率性数据结构,用于判断某个元素是否在一个集合中。它具有很高的空间效率和查询效率,但可能存在误判(即某些不存在的元素可能被错误地判断为存在)。布隆过滤器适合用来拦截缓存穿透。
实现步骤:
- 在启动时,将所有可能的有效键加载到布隆过滤器中。
- 每次查询时,先通过布隆过滤器判断键是否存在。如果布隆过滤器判断不存在,则直接返回空结果,而不查询数据库。
- 如果布隆过滤器判断存在,再查询缓存或数据库。
Java代码示例:
使用第三方库 Guava 的布隆过滤器:
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import redis.clients.jedis.Jedis;
public class BloomFilterExample {
private BloomFilter<String> bloomFilter;
private Jedis jedis;
public BloomFilterExample(Jedis jedis, int expectedInsertions) {
this.jedis = jedis;
bloomFilter = BloomFilter.create(Funnels.stringFunnel(), expectedInsertions);
// 初始化布隆过滤器,通常是从数据库加载所有键
initializeBloomFilter();
}
private void initializeBloomFilter() {
// 假设从数据库加载所有有效键
bloomFilter.put("existing_key1");
bloomFilter.put("existing_key2");
}
public String getData(String key) {
if (!bloomFilter.mightContain(key)) {
return null; // 直接返回,防止缓存穿透
}
String value = jedis.get(key);
if (value != null) {
return value;
}
// 模拟数据库查询
value = queryFromDatabase(key);
if (value != null) {
jedis.set(key, value);
}
return value;
}
private String queryFromDatabase(String key) {
// 模拟数据库查询
// return null if not found
return null;
}
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
BloomFilterExample example = new BloomFilterExample(jedis, 10000);
String value = example.getData("nonexistent_key");
System.out.println("Value: " + value);
}
}
3. 参数校验与拦截
对于某些明确格式的请求,可以在应用层进行参数校验或拦截非法请求,避免这些请求直接进入缓存或数据库。例如,对于用户ID类的请求,可以先判断用户ID是否合法,如果不合法,直接返回而不访问缓存和数据库。
实现步骤:
- 在查询之前进行参数校验。
- 如果请求参数不合法,直接返回,不查询缓存和数据库。
Java代码示例:
public class ParameterValidationExample {
public String getData(String userId) {
if (!isValidUserId(userId)) {
return null; // 直接返回,防止无效请求穿透
}
// 继续正常的缓存和数据库查询逻辑
// ...
return "data";
}
private boolean isValidUserId(String userId) {
// 假设用户ID必须是纯数字且长度为10
return userId.matches("\\d{10}");
}
public static void main(String[] args) {
ParameterValidationExample example = new ParameterValidationExample();
String value = example.getData("invalid_user_id");
System.out.println("Value: " + value); // null
}
}
总结
缓存穿透是 Redis 缓存系统中常见的问题之一,会导致大量请求穿透缓存直接打到数据库,影响系统性能。通过以下几种方法可以有效防止缓存穿透:
- 缓存空对象:对于查询结果为空的请求,将空结果缓存,并设置合理的过期时间。
- 使用布隆过滤器:在查询缓存和数据库之前,通过布隆过滤器判断键是否存在,拦截不存在的键。
- 参数校验与拦截:在应用层进行参数校验,拦截不合法的请求,避免不必要的缓存查询和数据库访问。
这些方法可以单独使用,也可以组合使用,根据具体业务场景选择合适的防止缓存穿透的策略。