java-redis-穿透
Redis 缓存穿透是指当请求的数据在缓存和数据库中都不存在时,用户每次请求都会直接查询数据库,导致缓存失效,无法发挥作用。这种情况下,用户发出的每个请求都绕过了缓存,直接打到了数据库,可能导致数据库压力骤增,甚至崩溃。
在实际应用中,缓存穿透通常会由于用户发送恶意请求或非法数据请求导致。例如,用户传递的 id
永远不会在数据库中找到相应的数据(如负数 ID 或过大的 ID)。由于这些 ID 不存在于数据库中,缓存也没有保存这些值,结果是每次请求都会直接访问数据库。
Redis 缓存穿透的常见解决方案
- 缓存空值:将不存在的结果也缓存起来,避免重复查询数据库。
- 布隆过滤器:通过布隆过滤器提前拦截非法请求,避免查询数据库。
- 参数校验:在查询数据库之前,先对请求参数进行校验,直接过滤掉非法请求。
接下来,我们逐一解释这些方案,并给出示例代码。
1. 缓存空值
当请求的键在数据库中不存在时,我们可以将这个“空结果”也缓存起来,并设置一个较短的过期时间,避免缓存永远存储无效值。下次再请求这个键时,直接从缓存中获取到空值,而不会访问数据库。
示例代码:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class CacheService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 模拟查询数据库
*/
public String getDataFromDB(String key) {
// 模拟数据库查询
if ("validKey".equals(key)) {
return "valueFromDB";
}
return null; // 模拟数据库中不存在
}
/**
* 查询数据,避免缓存穿透
*/
public String getData(String key) {
// 先从缓存中查询
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
// 缓存命中
return "从缓存获取: " + value;
}
// 如果缓存没有命中,则查询数据库
value = getDataFromDB(key);
if (value != null) {
// 如果数据库存在,则写入缓存
redisTemplate.opsForValue().set(key, value, 10, TimeUnit.MINUTES);
return "从数据库获取: " + value;
} else {
// 如果数据库不存在,则将空值缓存,并设置短期过期时间
redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
return "数据不存在,缓存空值";
}
}
}
在这个例子中,当数据库中找不到数据时,缓存会存储一个空字符串,防止后续的重复查询。可以根据实际场景设置空值的过期时间,例如设置为 5 分钟。这样可以减少数据库的压力。
2. 使用布隆过滤器
布隆过滤器(Bloom Filter)是一种高效的概率数据结构,用于快速判断某个元素是否存在于一个集合中。通过将所有合法的键加入布隆过滤器中,可以在查询数据库前先判断请求是否有意义,如果布隆过滤器认为该键不存在,则可以直接返回,不再查询缓存和数据库。
布隆过滤器的特点:
- 空间效率高:布隆过滤器使用哈希函数和位数组来表示元素的存在状态,空间占用很小。
- 存在误判:布隆过滤器可能会误判某个不存在的元素为存在(即假阳性),但不会误判存在的元素为不存在。
示例代码:
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;
@Service
public class CacheServiceWithBloomFilter {
@Autowired
private StringRedisTemplate redisTemplate;
private BloomFilter<String> bloomFilter;
// 初始化布隆过滤器
public CacheServiceWithBloomFilter() {
// 创建布隆过滤器,设置预计插入元素数量为10000,误判率为0.01
this.bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 10000, 0.01);
// 将已有数据添加到布隆过滤器
bloomFilter.put("validKey");
// 可以继续添加其他已知合法的键
}
/**
* 模拟查询数据库
*/
public String getDataFromDB(String key) {
if ("validKey".equals(key)) {
return "valueFromDB";
}
return null;
}
/**
* 查询数据,使用布隆过滤器避免缓存穿透
*/
public String getData(String key) {
// 先通过布隆过滤器判断该key是否可能存在
if (!bloomFilter.mightContain(key)) {
return "该数据不存在(布隆过滤器拦截)";
}
// 如果布隆过滤器认为存在,继续查询缓存
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
return "从缓存获取: " + value;
}
// 缓存未命中,查询数据库
value = getDataFromDB(key);
if (value != null) {
// 数据库存在,写入缓存
redisTemplate.opsForValue().set(key, value, 10, TimeUnit.MINUTES);
return "从数据库获取: " + value;
} else {
// 数据库不存在,缓存空值
redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
return "数据不存在,缓存空值";
}
}
}
在这个例子中,布隆过滤器用于快速判断请求的键是否可能存在。如果布隆过滤器判断该键不可能存在,则直接返回“数据不存在”,避免不必要的缓存和数据库查询。
3. 参数校验
对于明显不合法的请求参数(例如负数 ID、过大的 ID 等),可以在请求到达 Redis 或数据库之前直接进行参数校验,避免不合法的请求进入后端系统。
示例代码:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class CacheServiceWithValidation {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 模拟查询数据库
*/
public String getDataFromDB(String key) {
if ("validKey".equals(key)) {
return "valueFromDB";
}
return null;
}
/**
* 查询数据,增加参数校验
*/
public String getData(String key) {
// 进行参数合法性校验,过滤掉明显不合法的请求
if (key == null || key.length() == 0 || key.length() > 20) {
return "请求参数不合法";
}
// 查询缓存
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
return "从缓存获取: " + value;
}
// 缓存未命中,查询数据库
value = getDataFromDB(key);
if (value != null) {
redisTemplate.opsForValue().set(key, value, 10, TimeUnit.MINUTES);
return "从数据库获取: " + value;
} else {
redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
return "数据不存在,缓存空值";
}
}
}
在这个例子中,首先对请求的 key
进行参数校验,确保请求是合法的。如果 key
为空、长度过长等情况,直接返回错误信息,避免查询缓存和数据库。
Redis 穿透、击穿和雪崩
除了缓存穿透,还有两个常见的缓存问题:
-
缓存击穿:缓存中没有但数据库中有的数据,并且该数据在短时间内有大量请求。如果这类请求同时访问数据库,可能导致数据库压力激增。
解决方案:使用互斥锁(如分布式锁)防止多个线程同时查询数据库,或者为热点数据设置较长的过期时间。
-
缓存雪崩:由于缓存服务器宕机或大量缓存同时失效,导致大量请求直接打到数据库,给数据库造成很大压力。
解决方案:为缓存设置不同的过期时间(缓存失效时间的随机化),避免缓存集中失效;增加缓存节点的冗余。
总结
缓存穿透是 Redis 缓存系统中的一个常见问题,它会导致大量请求绕过缓存直接访问数据库,给数据库带来巨大的压力。针对缓存穿透,我们可以使用以下方案:
- **缓存空值
**:将数据库中不存在的值缓存起来,防止重复查询。
2. 布隆过滤器:在查询缓存和数据库之前,利用布隆过滤器判断请求是否合法,从而减少不必要的数据库查询。
3. 参数校验:在应用层进行参数合法性校验,直接拦截明显非法的请求。