缓存雪崩
雪崩的现象:
DB 接受到的请求量过大,导致 DB 下线或数据查询变慢,并且相关依赖该库的接口都抛出错误,导致大量服务挂掉。短时间内重启后,依然会被大量请求给打下线,导致一段时间内服务不可用。
原因:
在较短时间段内,大量的缓存数据集中过期了。如在代码中,短时间内对大批量的热点数据设置了 30min 过期,而 30min 后,大批量的热点数据被清理掉,导致大量的数据请求到 DB 。
解决方案:
-
对热点数据的过期时间,再添加一个随机值( Math.random()*1000 ),避免出现极短的时间内大量缓存数据被删除的情况。
-
限流降级,控制进入的流量大小,降级部分请求即不处理
缓存击穿
击穿的原因:
缓存击穿是指某一个热点数据过期了,但同时又有大批量其他请求来访问该数据,导致大量的请求到了 DB 上。它与缓存雪崩相似,缓存雪崩指的是多个 key ,而缓存击穿指的是某一个 key。
解决方案:
- 适当的延长该类型 key 过期时间
- 使用分布式锁,让一个请求去加载数据到缓存中,避免大量请求到达 DB。实现代码可参考如下:
private String getKey(String key){
String value = stringRedisTemplate.opsForValue().get(key);
if(StringUtils.isEmpty(value)){//缓存过期了
RLock lockKey = redisson.getLock("lockKey");
try {
//4秒内等待尝试获取锁;如果获取成功,加3s的锁(该类型的锁每隔3s/3s,检测锁是否存在;如果锁存在重新设置超时时间为3s,如果不存在,则不做处理)。
boolean lockSuccess = lockKey.tryLock(4, 3, TimeUnit.SECONDS);
if(lockSuccess){//加锁成功,当前线程去读取 DB 并设置缓存
value = db.get(key);
stringRedisTemplate.opsForValue().set(key, value, 10,TimeUnit.SECONDS);
}else {//加锁失败,证明已有其他线程执行了上面两行代码
TimeUnit.MILLISECONDS.sleep(200);
getKey(key);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockKey.unlock();
}
}
return value;
}
缓存穿透
穿透的现象:
穿透是大量异常参数请求打到了 DB,例如对大量不存在的数据进行查询的请求。对于小一点的单机系统,利用 Postman + 缓存穿透的漏洞就可以把 DB 打挂。
原因:
查询一个一定不存在的数据,由于缓存不命中,导致每次都要到持久化存储层去查询数据。比如使用 id < 0 或 id = Integer.MAX_VALUE来查询数据。
解决方案:
- 对于接受查询参数的接口,做非法参数校验,避免请求到数据库。
- 对缓存和 DB 中都不存在的数据,考虑将对应的缓存 value 设置为 null,并将有效期设置短一点。使用该方案时需要考虑,避免缓存与 DB 数据不一致的问题,需要在数据设值时,去删除或者刷新缓存中对应的值。
- 缓存穿透是建立在大量请求的基础上,可以考虑在网关层(Nginx)限制单个 ip 的 QPS 来控制部分异常情况。
- 可以使用 Bloom Filter 过滤数据不存在的请求,Cuckoo Filter(布谷鸟过滤器)可以更好的解决缓存穿透问题。更多关于 Bloom Filter 的介绍
缓存预热
缺少预热的现象:
在某个考试系统中,开考时间为 10 点整,大量考生该时刻涌入,部分考生在获取题目时,需要读取 DB,导致 DB 压力过大,考生被迫等待卡顿一段时间。
原因
题库数据在使用前,未被加载到 Redis ,DB 压力过大导致响应变慢。
解决方案:
为避免出现用户在使用热点数据时,先去 DB 查,然后再保存到 Redis 中的过程,可以结合 缓存预热和分布式锁 来添加缓存。