Redis中的缓存击穿、缓存穿透和缓存雪崩分别是什么?
缓存击穿:当热点数据过期时,很多请求同时去查询数据库,可能让数据库压力过大,甚至崩溃。
缓存穿透:请求的内容既不在缓存里也不在数据库里,这些无效请求直接访问数据库,可能让数据库被“击穿”。
缓存雪崩:当很多缓存同时失效或 Redis 宕机时,所有请求直接打到数据库,可能引起系统崩溃。
解决这些问题可以通过 预防策略、多级缓存 和 限流熔断 等技术手段来避免。
详细解释以及解决措施
缓存击穿
原理
缓存击穿发生在一个热点数据突然过期的情况下,此时所有针对该数据的请求都绕过缓存,直接进入数据库查询。这种情况常见于高并发场景,例如秒杀活动。
风险
- 数据库压力激增:高并发请求直接打到数据库,可能导致数据库响应缓慢甚至宕机。
- 服务不可用:数据库压力过大时,服务整体可能不可用。
解决方案
- 缓存预热:在热点数据正式对外服务前,提前将其加载到缓存中。
- 互斥锁:多个线程访问同一缓存键时,通过锁机制保证只有一个线程能够查询数据库并更新缓存。
- 永不过期:设置热点数据永不过期,通过异步后台线程定期刷新缓存内容。
- 逻辑过期:在缓存对象中维护一个过期时间的字段,当查询缓存时发现已过期则获取互斥锁(例如 Redis 中的 setnx)并开启一个新线程重建缓存数据并将释放互斥锁的逻辑放在新线程中,原线程返回过期数据。当在缓存重建时有其他线程访问缓存并发现数据过期时获取互斥锁失败则直接返回过期数据。这种方法通常应用在对可用性较高的场景。
代码示例
互斥锁解决缓存击穿
import java.util.concurrent.locks.ReentrantLock;
public class CacheBreakdownSolution {
private static final String CACHE_KEY = "hot_data";
private static final ReentrantLock lock = new ReentrantLock();
public static String getHotData() {
String cacheValue = Redis.get(CACHE_KEY);
if (cacheValue == null) {
lock.lock(); // 加锁避免并发访问数据库
try {
cacheValue = Redis.get(CACHE_KEY);
if (cacheValue == null) {
cacheValue = Database.query("SELECT * FROM hot_data");
Redis.set(CACHE_KEY, cacheValue, 60); // 设置过期时间
}
} finally {
lock.unlock();
}
}
return cacheValue;
}
}
逻辑过期
在缓存对象中维护一个过期时间的字段,当查询缓存时发现已过期则获取互斥锁(例如 Redis 中的 setnx)并开启一个新线程重建缓存数据并将释放互斥锁的逻辑放在新线程中,原线程返回过期数据。当在缓存重建时有其他线程访问缓存并发现数据过期时获取互斥锁失败则直接返回过期数据。这种方法通常应用在对可用性较高的场景。
互斥锁解决缓存击穿和逻辑过期的优劣
缓存穿透
原理
缓存穿透指的是请求的数据在缓存和数据库中都不存在。无效请求绕过缓存,直接打到数据库,这种情况通常由恶意攻击或不合理的查询参数引起。
风险
- 恶意攻击:恶意用户发送大量无效请求,导致缓存失效。
- 数据库过载:频繁的无效请求会让数据库长时间处于高负载状态。
解决方案
- 参数校验:在查询前验证参数合法性,例如检查 ID 是否为正数。
- 布隆过滤器:快速判断数据是否存在,拦截不存在的请求。
- 缓存空值:将查询结果为空的数据也缓存起来,避免频繁查询。
代码示例
布隆过滤器解决缓存穿透
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class CachePenetrationSolution {
private static final BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 100000);
static {
// 初始化布隆过滤器
for (int i = 1; i <= 100000; i++) {
bloomFilter.put(i);
}
}
public static String getData(int id) {
if (!bloomFilter.mightContain(id)) {
return "Data does not exist";
}
String cacheValue = Redis.get(String.valueOf(id));
if (cacheValue == null) {
cacheValue = Database.query("SELECT * FROM data WHERE id=" + id);
Redis.set(String.valueOf(id), cacheValue == null ? "null" : cacheValue, 60);
}
return cacheValue;
}
}
缓存雪崩
原理
缓存雪崩是指由于大量缓存同时失效或 Redis 服务宕机,大量请求绕过缓存,直接进入数据库查询,导致数据库压力骤增,甚至引发服务不可用。
风险
- 数据库宕机:短时间内大量请求直接打到数据库,超出数据库承载能力。
- 服务级联故障:数据库宕机后,可能导致依赖数据库的其他服务也不可用。
解决方案
- 随机过期时间:避免大量缓存同时失效。
- 多级缓存:在本地、Redis 和数据库之间引入多级缓存架构。
- 熔断限流:通过限流保护机制减少对数据库的访问。
代码示例
随机过期时间解决缓存雪崩
import java.util.Random;
public class CacheSnowballSolution {
private static final Random random = new Random();
public static void setData(String key, String value) {
int expireTime = 60 + random.nextInt(30); // 60 ~ 90 秒随机过期
Redis.set(key, value, expireTime);
}
}
知识拓展
布隆过滤器的应用
布隆过滤器是一种高效的数据结构,用于判断某个元素是否存在。虽然存在误判率,但适合在高并发和海量数据场景中使用,比如防止缓存穿透。
代码实现布隆过滤器
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class BloomFilterExample {
public static void main(String[] args) {
BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), 1000, 0.01);
filter.put(1);
System.out.println(filter.mightContain(1)); // true
System.out.println(filter.mightContain(2)); // false
}
}
多级缓存架构
多级缓存通过在本地、Redis 和数据库之间分层,降低缓存服务宕机对系统的影响。
多级缓存示例架构
Client -> Local Cache (Caffeine) -> Redis -> Database
代码示例
Caffeine 本地缓存与 Redis 结合
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
public class MultiLevelCache {
private static final Cache<String, String> localCache = Caffeine.newBuilder().maximumSize(1000).build();
public static String getData(String key) {
String value = localCache.getIfPresent(key);
if (value == null) {
value = Redis.get(key);
if (value != null) {
localCache.put(key, value);
}
}
return value;
}
}
总结
- 缓存击穿:热点数据过期,导致大量请求直接查询数据库。
- 缓存穿透:无效请求绕过缓存直接打到数据库。
- 缓存雪崩:大量缓存同时失效或 Redis 宕机引发系统崩溃。
通过优化缓存策略、引入布隆过滤器、多级缓存架构等技术,可以有效提升系统的高可用性和容错能力!