什么是 Redis 缓存穿透、缓存击穿、缓存雪崩?
在使用 Redis 缓存时,可能会遇到一些缓存问题,最常见的包括缓存穿透、缓存击穿和缓存雪崩。
1. 缓存穿透
缓存穿透指的是在缓存中没有找到需要的值,每次请求都会访问数据库,而由于数据库中也不存在需要的数据,导致每次请求返回的结果都为空,从而浪费了大量的服务端资源。
这种情况可以通过添加布隆过滤器(BloomFilter)进行处理,将所有可能的查询参数哈希后存储起来,每次查询前先判断哈希值是否存在于布隆过滤器中,若不在则直接返回空结果。
2. 缓存击穿
缓存击穿指的是一个原本存在的 key,在缓存失效的一刹那,同时有大量的并发请求过来,这些请求发现缓存中不存在该 key,于是就直接请求了数据库,从而导致了数据库瞬时压力过大甚至宕机的情况。
这种情况可以通过为热点数据设置永不过期的方式解决,一般会使用 Redis 的 setnx(SET if Not eXists)命令,将缓存数据永久保存在 Redis 中。
3. 缓存雪崩
缓存雪崩指的是缓存中大量的 key,在同一时刻失效,导致大量的请求直接打到了数据库,从而导致数据库瞬时压力过大甚至宕机的情况。
这种情况可以通过加入一个随机过期时间解决,不同的 key 分别设置不同的过期时间来保证不会在同一时间失效。也可以使用 Redis Cluster 技术对 Redis 数据库进行集群化部署,避免单点故障。
SpringBoot 中如何解决 Redis 缓存穿透、缓存击穿、缓存雪崩?
在 SpringBoot 中,我们可以通过配置 RedisTemplate 来实现 Redis 缓存的操作。同时,Spring 提供了 CacheManager 和 Cache 接口用于管理缓存。具体方案如下:
1. 解决 Redis 缓存穿透
1.1 添加布隆过滤器
首先,我们需要在项目中添加布隆过滤器,这里我们使用 Google Guava 库提供的 BloomFilter 实现:
代码语言:java复制@Bean
public BloomFilter<String> initBloomFilter() {
int expectedInsertions = 1000000;
double fpp = 0.001;
return BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), expectedInsertions, fpp);
}
这里的 expectedInsertions
是预计添加的元素个数,fpp
表示期望的误差率。可以通过调整这两个参数来控制 BloomFilter 的性能和空间占用。
然后,在查询缓存时,我们需要先将查询参数进行哈希操作并判断是否存在于布隆过滤器中:
代码语言:java复制@Autowired
private BloomFilter<String> bloomFilter;
public Object query(String key) {
// 先判断 key 是否存在于布隆过滤器中
if (!bloomFilter.mightContain(key)) {
return null;
}
// 查询缓存
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
Object result = operations.get(key);
// 如果缓存中没有找到,则查询数据库
if (result == null) {
// 查询数据库
result = queryFromDB(key);
// 将查询结果加入缓存,并设置过期时间
if (result != null) {
operations.set(key, result, 5, TimeUnit.MINUTES);
}
}
return result;
}
1.2 添加空值缓存
另外,由于缓存穿透可能会导致大量的请求直接打到数据库,因此我们还可以在缓存中添加空值来避免重复查询。当查询的 key 对应的 value 为 null 时,我们可以将其缓存到 Redis 中,并设置一个较短的过期时间:
代码语言:java复制public Object query(String key) {
// 先从缓存中查询
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
Object result = operations.get(key);
// 如果缓存中没有找到,则查询数据库
if (result == null) {
// 查询数据库
result = queryFromDB(key);
// 将查询结果加入缓存,并设置过期时间
if (result != null) {
operations.set(key, result, 5, TimeUnit.MINUTES);
} else {
// 缓存空值,避免重复查询
operations.set(key, "", 1, TimeUnit.MINUTES);
}
}
// 如果查询结果是空字符串,则返回 null
return "".equals(result) ? null : result;
}
2. 解决 Redis 缓存击穿
为了避免缓存击穿,我们可以将一些热点数据永久保存在 Redis 中。同时,我们需要注意设置合适的过期时间,以免占用过多的内存。
代码语言:java复制@Cacheable(value = "user", key = "#id", unless = "#result == null")
public User getUser(Long id) {
// 先从缓存中查询
User user = userDao.selectById(id);
// 如果缓存中没有找到,则查询数据库
if (user == null) {
// 查询数据库
user = queryFromDB(id);
// 将查询结果加入缓存,并永不过期
if (user != null) {
redisTemplate.opsForValue().set("user:" id, user);
}
}
return user;
}
3. 解决 Redis 缓存雪崩
为了避免缓存雪崩,我们可以在设置缓存时加入一个随机的过期时间,这样可以将原本同时失效的缓存数据错开。
代码语言:java复制@Cacheable(value = "user", key = "#id", unless = "#result == null")
public User getUser(Long id) {
// 先从缓存中查询
User user = redisTemplate.opsForValue().get("user:" id);
// 如果缓存中没有找到,则查询数据库
if (user == null) {
// 查询数据库
user = queryFromDB(id);
// 将查询结果加入缓存,并设置随机过期时间,避免同时失效
if (user != null) {
long expireTime = new Random().nextInt(300) 600;
redisTemplate.opsForValue().set("user:" id, user, expireTime, TimeUnit.SECONDS);
}
}
return user;
}
总结
Redis 是一个高性能的缓存工具,在处理大量数据时非常有用。但是,当面对大规模缓存时,可能会产生一些缓存问题,如缓存穿透、缓存击穿和缓存雪崩等。针对这些问题,我们可以使用 BloomFilter 等技术来优化查询,也可以设置永不过期、随机过期时间等方式来避免缓存击穿和缓存雪崩。同时,在 SpringBoot 中,我们可以使用 CacheManager 和 Cache 接口来管理缓存,使得缓存的操作更加简单方便。