一 Redis雪崩、穿透和击穿
1. Redis雪崩:
Redis雪崩是指在某一时刻,缓存中大量的缓存数据同时失效或过期,导致大量的请求直接打到后端数据库,导致数据库负载剧增,引发性能问题甚至崩溃。这通常是因为缓存数据的过期时间设置过于集中,或者在同一时间段内大量缓存同时失效造成的。
2. Redis穿透
Redis穿透是指恶意或者异常请求查询一个不存在于缓存和数据库中的数据,导致每次请求都会直接访问数据库,增加了数据库负担。这可能是攻击者故意进行的,也可能是由于业务逻辑问题造成的。
3. Redis击穿:
Redis击穿是指某个热点数据突然失效或被删除,而此时大量请求正好同时访问该热点数据,导致这些请求都直接打到数据库上,导致数据库压力激增。与雪崩不同,击穿是因为某个特定的缓存数据失效导致。
示例:
让我们以一个简单的Java代码示例来说明Redis雪崩、穿透和击穿的概念。
假设有一个电影信息查询系统,用户可以根据电影ID查询电影信息。我们使用Redis作为缓存来存储电影信息,但是只对热门电影设置了缓存,其他电影没有被缓存。
@Service
public class MovieService {
@Autowired
private MovieRepository movieRepository;
@Autowired
private Jedis jedis;
public Movie getMovieInfo(String movieId) {
String cacheKey = "movie:" + movieId;
String cachedInfo = jedis.get(cacheKey);
if (cachedInfo == null) {
Movie movie = movieRepository.findById(movieId);
if (movie != null) {
jedis.setex(cacheKey, 3600, movie.toString()); // 缓存1小时
return movie;
}
}
return Movie.fromString(cachedInfo);
}
}
Redis雪崩示例:
假设在某一时刻,缓存中存储了很多电影信息,这些缓存在同一时间内同时失效,导致大量请求直接访问数据库,造成数据库压力激增。
Redis穿透示例:
有一个恶意用户不断发送不存在的电影ID,每次请求都会绕过缓存,直接查询数据库,导致数据库压力增加。
Redis击穿示例:
假设某个热门电影的缓存在某个时间点失效,而在这个时间点正好有大量用户同时查询该电影信息,导致所有请求直接访问数据库,造成数据库压力激增。
二 解决方案
2.1 对缓存数据的过期时间进行随机化,避免集中失效。
-
选择随机时间范围: 首先,你需要选择一个适当的随机时间范围,用于分散缓存数据的过期时间。例如,你可以选择在原始过期时间基础上添加一个随机的秒数,这样每个缓存项的过期时间就会稍微有所不同。
-
生成随机时间: 在获取缓存数据时,生成一个随机的秒数,然后将其添加到原始过期时间上,得到一个新的过期时间。
-
设置缓存数据: 将缓存数据存储到Redis中,并设置使用上一步生成的新过期时间。
@Service public class CacheService { @Autowired private Jedis jedis; public String getCachedData(String key) { String cachedData = jedis.get(key); if (cachedData == null) { // 查询数据库获取数据 String dbData = Database.queryData(key); if (dbData != null) { // 生成随机的过期时间(在1小时基础上随机增加0-300秒) int originalExpireTime = 3600; // 1小时的秒数 int randomSeconds = new Random().nextInt(300); // 0到300秒的随机数 int cacheDuration = originalExpireTime + randomSeconds; // 将数据存储到缓存并设置随机过期时间 jedis.setex(key, cacheDuration, dbData); return dbData; } } return cachedData; } }
2.2 使用布隆过滤器来过滤恶意请求,防止缓存穿透。
使用布隆过滤器来过滤恶意请求,以防止缓存穿透是一种常见的防御策略。布隆过滤器是一种数据结构,用于判断一个元素是否存在于集合中,它可以高效地进行快速查询,但可能会有一定的误判率。
下面是一个使用Spring Boot和布隆过滤器来防止缓存穿透的详细举例:
步骤:
引入依赖: 在Spring Boot项目中,添加所需的依赖,包括Spring Boot、Jedis和Google Guava(用于实现布隆过滤器)。
初始化布隆过滤器: 在启动时初始化一个布隆过滤器,用于存储已查询的缓存键。
查询缓存数据: 在获取数据之前,首先检查布隆过滤器,如果缓存键可能存在,则再查询缓存。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import redis.clients.jedis.Jedis;
@Service
public class CacheService {
private final Jedis jedis;
private final BloomFilter<String> bloomFilter;
@Autowired
public CacheService(Jedis jedis) {
this.jedis = jedis;
this.bloomFilter = BloomFilter.create(Funnels.stringFunnel(), 1000, 0.01); // 初始化布隆过滤器
}
public String getCachedData(String key) {
if (!bloomFilter.mightContain(key)) { // 判断是否可能存在于集合中
return null; // 不再查询缓存和数据库,直接返回null
}
String cachedData = jedis.get(key);
if (cachedData == null) {
// 查询数据库获取数据
String dbData = Database.queryData(key);
if (dbData != null) {
jedis.setex(key, 3600, dbData); // 缓存1小时
bloomFilter.put(key); // 将键添加到布隆过滤器中
return dbData;
}
}
return cachedData;
}
}
2.3 使用互斥锁(例如分布式锁)来防止击穿,只允许一个请求去查询数据库,其他请求等待或直接使用缓存。
使用互斥锁(分布式锁)来防止击穿是一种常见的策略,可以确保在缓存失效的情况下,只有一个请求能够去查询数据库,其他请求需要等待该请求完成或直接使用缓存。下面是一个使用Spring Boot和Jedis实现分布式锁来防止击穿的代码示例:
步骤:
1、引入依赖: 在Spring Boot项目中,添加所需的依赖,包括Spring Boot和Jedis。
2、获取分布式锁: 在查询数据库之前,使用分布式锁来确保只有一个请求能够进行数据库查询。
3、释放分布式锁: 在查询完成后,释放分布式锁,让其他请求能够继续执行
@Service
public class CacheService {
@Autowired
private Jedis jedis;
public String getCachedData(String key) {
String cachedData = jedis.get(key);
if (cachedData == null) {
// 尝试获取分布式锁,设置锁的过期时间,防止死锁
String lockKey = "lock:" + key;
String lockValue = "lockValue";
SetParams params = new SetParams().ex(60).nx(); // 设置60秒过期时间,只有不存在时才设置
String acquiredLock = jedis.set(lockKey, lockValue, params);
if (acquiredLock != null) {
try {
// 查询数据库获取数据
String dbData = Database.queryData(key);
if (dbData != null) {
jedis.setex(key, 3600, dbData); // 缓存1小时
return dbData;
}
} finally {
// 释放分布式锁
jedis.del(lockKey);
}
} else {
// 等待一段时间后重新查询缓存
try {
Thread.sleep(200); // 可以根据实际情况调整等待时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 重新查询缓存
cachedData = jedis.get(key);
}
}
return cachedData;
}
}
2.4 合理设置缓存策略,确保热门数据始终保持缓存,避免缓存雪崩。
确保热门数据始终保持缓存,避免缓存雪崩,需要采取一些合理的缓存策略。以下是一些常见的合理方案:
1. 定时刷新缓存: 使用定时任务或调度器,定期刷新热门数据的缓存。这可以确保缓存中的数据始终保持最新,避免数据过期。
2. 永不过期策略:对于热门数据,可以设置永不过期的缓存策略。但要注意,如果热门数据发生变化,需要手动更新缓存。
3. 热点数据预加载:在应用启动时,预先加载热门数据到缓存中,确保缓存中存在最常用的数据。
4. 基于访问频率的过期策略: 根据数据的访问频率动态调整过期时间。访问频率高的数据设置较长的过期时间,访问频率低的数据设置较短的过期时间。
5. 分布式锁控制:** 在缓存失效时,使用分布式锁来防止多个请求同时查询数据库,确保只有一个请求进行查询并更新缓存。
6. 降级策略: 如果缓存失效,可以暂时使用降级策略,例如返回默认值或静态数据,以避免直接访问数据库。
7. 多级缓存: 使用多级缓存架构,将热门数据存储在多个缓存层中,例如内存缓存和分布式缓存,以提高数据的访问速度和稳定性。
8. 请求合并: 对于同时涌入的大量请求,可以考虑将它们合并成一个请求,只查询一次数据库,然后将结果分发给多个请求。
9. 缓存预热: 在系统负载较低的时候,提前将热门数据加载到缓存中,以减少在高负载时的数据库压力。
10. 动态缓存策略: 根据系统的实际情况,动态调整缓存策略,例如根据时间段、节假日等因素来设置不同的缓存策略。
选择合适的方案取决于你的业务需求和系统特点。通常,结合多个方案可以更好地保护热门数据,避免缓存雪崩问题。