目录
缓存击穿
概念
缓存击穿的原因
缓存击穿的影响
缓存击穿的应对措施
设置分布式锁
提前更新缓存
请求分级和降级
缓存穿透
概念
缓存穿透的原因
缓存穿透的应对措施
缓存空值
布隆过滤器
限流和黑名单
缓存雪崩
缓存雪崩概念
缓存雪崩的原因
应对措施
缓存过期时间随机化:
多级缓存架构
限流
预热缓存
缓存击穿
概念
缓存击穿是指缓存中不存在但数据库中存在的数据请求,由于缓存没有命中,导致请求直接落到数据库上,造成数据库压力骤增。缓存击穿常常发生在某些热点数据上,比如某个高访问频率的热点键突然失效时,大量请求直接穿透缓存访问数据库。
缓存击穿的原因
热点数据缓存失效:高访问频率的热点数据在缓存中失效,导致大量请求直接访问数据库。
缓存设置不当:如缓存策略不合理,过期时间设置过短等
突发流量:突发性的大量请求几种访问某个热点数据,导致缓存来不及重建,直接击穿到数据库。
缓存击穿的影响
数据库压力骤增:大量请求直接访问数据库,可能导致数据库负载过高甚至崩溃。
响应延迟增加:缓存击穿会导致请求需要经过数据库查询,响应时间变长,影响用户体验。
系统稳定性降低:数据库过载可能引发系统不稳定,甚至导致服务不可用。
缓存击穿的应对措施
设置分布式锁
在缓存失效时,通过加锁机制,确保只有一个线程去加载数据并更新缓存,其他线程等待锁释放后再读取缓存。
提前更新缓存
在缓存失效前主动更新缓存。可以使用定时任务或异步任务,在缓存即将过期时提前刷新缓存。
请求分级和降级
- 针对缓存击穿的情况,可以对请求进行分级处理,优先保证核心请求。
- 对非核心请求进行降级处理,如返回默认值或错误提示。
缓存穿透
概念
缓存穿透是指请求的数据既不在缓存中,也不存在于数据库中,导致每次请求都直接达到数据库。由于这些请求无法通过缓存过滤掉,所有的请求都到达数据库。可能会对数据库造成巨大压力。
缓存穿透的原因
恶意攻击:攻击者故意构造大量不存在的key进行请求,绕过缓存,直接打到数据库,造成数据库压力骤增。
业务设计缺陷:正常业务逻辑中,存在大量的不存在的数据请求,未合理处理,导致频繁穿透缓存。
用户错误输入:用户输入错误或者不规范的数据,导致请求的数据在缓存和数据库中都不存在。
缓存穿透的应对措施
缓存空值
- 对于查询结果为空的数据,也将其缓存起来,避免下一次请求再次穿透过数据库。
String key = "nonExistingKey";
String value = redis.get(key);
if (value == null) {
value = db.get(key);
if (value == null) {
// 将空值写入缓存,并设置合理的过期时间
redis.set(key, "", 60); // 60秒过期
} else {
redis.set(key, value, 300); // 300秒过期
}
}
布隆过滤器
- 使用布隆过滤器在缓存前进行一次快速判断,如果布隆过滤器判断该 key 不存在,则直接返回,不再查询缓存和数据库。
// 初始化布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), expectedInsertions);
// 添加存在的key到布隆过滤器
bloomFilter.put("existingKey1");
bloomFilter.put("existingKey2");
String key = "nonExistingKey";
if (!bloomFilter.mightContain(key)) {
return null; // 直接返回,不查询缓存和数据库
}
// 正常的缓存和数据库查询流程
String value = redis.get(key);
if (value == null) {
value = db.get(key);
redis.set(key, value);
}
限流和黑名单
- 对于明显的恶意请求,可以采取限流和黑名单策略,限制其访问频率或直接拒绝其请求。
String clientIp = getClientIp();
if (isBlacklisted(clientIp)) {
return null; // 直接返回,不查询缓存和数据库
}
// 正常的缓存和数据库查询流程
String key = "someKey";
String value = redis.get(key);
if (value == null) {
value = db.get(key);
redis.set(key, value);
}
缓存雪崩
缓存雪崩概念
缓存雪崩是指由于大量缓存数据在同一时间过期或失效,导致后续的所有请求都直接访问数据库,瞬间增加数据库的负载。与缓存击穿不同,缓存雪崩通常影响的是大批量的缓存数据,而不是单个热点数据。
缓存雪崩的原因
- 缓存过期时间设置不合理:大批量缓存键设定了相同或相近的过期时间,导致它们在同一时间点失效。
- 突发流量:在流量高峰期,如果缓存失效,短时间内会有大量请求同时涌入数据库。
- 缓存服务宕机:缓存服务(如 Redis 实例)出现宕机或重启,导致所有缓存失效,所有请求直接击中数据库。
应对措施
缓存过期时间随机化:
- 在设置缓存过期时间时,添加一定的随机时间,使得不同的缓存键有不同的过期时间,避免大量缓存键在同一时间失效。
// 伪代码示例
int baseExpireTime = 600; // 基础过期时间600秒
int randomTime = new Random().nextInt(300); // 随机附加300秒
int expireTime = baseExpireTime + randomTime;
redis.set(key, value, expireTime);
多级缓存架构
- 通过引入本地缓存(如 Guava Cache)和分布式缓存(如 Redis)相结合的多级缓存架构,减少直接访问数据库的请求数量。
// 伪代码示例
String key = "someKey";
String value = localCache.get(key);
if (value == null) {
value = redis.get(key);
if (value == null) {
value = db.get(key);
redis.set(key, value);
}
localCache.put(key, value);
}
限流
- 通过限流机制控制高并发请求的流量,防止因大量请求同时访问数据库而导致数据库崩溃。
// 伪代码示例
if (!rateLimiter.tryAcquire()) {
return "Service is busy, please try again later.";
}
String key = "someKey";
String value = redis.get(key);
if (value == null) {
value = db.get(key);
redis.set(key, value);
}
return value;
预热缓存
- 在高峰期来临之前,预先将热点数据加载到缓存中,避免在流量高峰期缓存失效导致的雪崩。
// 伪代码示例
@Scheduled(fixedRate = 60000) // 每分钟执行一次
public void preheatCache() {
String key = "hotKey";
String value = db.get(key);
redis.set(key, value);
}
希望本文能帮助大家更好地理解和应对 Redis 缓存击穿,缓存穿透,缓存雪崩问题。