在 Redis 中,缓存击穿、缓存穿透和缓存雪崩是常见的缓存问题,理解它们对于设计高性能和高可用的缓存系统至关重要。
1. 缓存击穿(Cache Breakdown)
缓存击穿发生在一个热点数据的缓存过期后,大量并发请求直接访问数据库,导致数据库瞬时压力剧增。这通常发生在高并发场景下,当某个热数据过期时,多个请求同时到达,并且发现缓存中没有数据,从而访问数据库。
解决方案:
- 使用互斥锁:在缓存失效后,只有一个请求能够访问数据库并更新缓存,其它请求等待缓存更新完成后再获取数据。
- 提前更新缓存:在缓存即将过期时,通过后台线程异步更新缓存,防止缓存过期。
示例:使用互斥锁来防止缓存击穿
import java.util.concurrent.locks.ReentrantLock;
public class CacheService {
private static final ReentrantLock lock = new ReentrantLock();
public String getValue(String key) {
// 1. 尝试从缓存中获取数据
String value = redisGet(key);
if (value != null) {
return value;
}
// 2. 缓存未命中,尝试获取锁
try {
lock.lock();
// 双重检查
value = redisGet(key);
if (value != null) {
return value;
}
// 3. 从数据库中获取数据
value = dbGet(key);
// 4. 将数据写入缓存
redisSet(key, value);
} finally {
lock.unlock();
}
return value;
}
private String redisGet(String key) {
// 模拟从 Redis 获取数据
return null;
}
private void redisSet(String key, String value) {
// 模拟将数据写入 Redis
}
private String dbGet(String key) {
// 模拟从数据库获取数据
return "databaseValue";
}
}
2. 缓存穿透(Cache Penetration)
缓存穿透指的是查询一个根本不存在的数据,由于缓存不命中,每次查询都会直接访问数据库。如果有恶意攻击,利用这一特点发送大量请求,会使数据库压力倍增。
解决方案:
- 缓存空值:对于查询不存在的数据,将空结果(例如
null
)缓存起来,设置一个较短的过期时间,防止重复查询。 - 布隆过滤器:在缓存之前,使用布隆过滤器来判断请求的 key 是否存在,减少对不存在数据的查询。
示例:使用缓存空值和布隆过滤器防止缓存穿透
import java.util.HashMap;
import java.util.Map;
public class CacheService {
private static final Map<String, String> cache = new HashMap<>();
private static final BloomFilter<String> bloomFilter = new BloomFilter<>(1000000, 0.01);
public String getValue(String key) {
// 1. 使用布隆过滤器判断 key 是否存在
if (!bloomFilter.mightContain(key)) {
return null;
}
// 2. 从缓存中获取数据
String value = cache.get(key);
if (value != null) {
return value;
}
// 3. 从数据库中获取数据
value = dbGet(key);
// 4. 如果数据库没有该数据,缓存空值
if (value == null) {
cache.put(key, null); // 缓存空值
return null;
}
// 5. 将数据写入缓存
cache.put(key, value);
return value;
}
private String dbGet(String key) {
// 模拟从数据库获取数据
return null;
}
}
3. 缓存雪崩(Cache Avalanche)
缓存雪崩是指缓存系统中的大量数据在同一时间失效,导致瞬时大量请求直接访问数据库,可能造成数据库压力过大甚至宕机。这与缓存击穿的区别在于,雪崩是大量缓存同时失效,而击穿是单一热点数据失效。
解决方案:
- 设置不同的过期时间:让缓存数据的过期时间随机分布,避免在同一时间大量缓存失效。
- 使用多级缓存:在本地和分布式缓存之间设置多级缓存,降低缓存雪崩的影响。
- 增加缓存容灾:使用如 Redis Cluster、分布式缓存、或降级策略等手段来提高缓存的容灾能力。
示例:通过设置不同的过期时间来防止缓存雪崩
import java.util.concurrent.ThreadLocalRandom;
public class CacheService {
public void setValue(String key, String value) {
// 1. 设置随机过期时间,避免缓存雪崩
int randomExpireTime = ThreadLocalRandom.current().nextInt(3600, 7200); // 随机过期时间1-2小时
redisSet(key, value, randomExpireTime);
}
private void redisSet(String key, String value, int expireTime) {
// 模拟将数据写入 Redis 并设置过期时间
}
public String getValue(String key) {
// 从缓存中获取数据
return redisGet(key);
}
private String redisGet(String key) {
// 模拟从 Redis 获取数据
return "cachedValue";
}
}