目录
一、缓存穿透
1.1 什么是缓存穿透
是指缓存和数据库中都没有的数据,导致请求直接打到数据库,如一些恶意请求,会让后端存储负载加大。通常可以在程序中统计总调用数、缓存层命中数,如果同一个key的缓存命中率很低,可能就是出现了缓存穿透。
1.2 出现的原因
- 自身业务代码或者数据的问题(如set和get的key不一致)。
- 一些恶意攻击、爬虫等造成大量空命中(爬取商城商品数据,超大循环递增商品的ID)。
1.3 如何解决
- 缓存空对象:从缓存中取不到的数据,在数据库中也没有取到,这时可以将key-value写为key-null。 缓存空对象会有两个问题:
第一,value为null 不代表不占用内存空间,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除,如30秒(设置太长会导致正常情况也没法使用)
第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如在过期时间内,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象(入库的时候去同步缓存数据)。
接口层根据具体业务场景增加校验,如用户鉴权校验,id做基础校验,id<=0或者大于某个范围的直接拦截。
- 使用布隆过滤器拦截(布隆过滤器有待整理)
方案 适用场景 维护成本 缓存空对象 数据命中率不高
数据频繁变化实时性高
代码维护简单
需要过多的内存空间
数据不一致问题
布隆过滤器 数据命中率不高
数据相对固定实时性低
代码维护复杂
缓存空间少
二、缓存击穿
2.1 什么是缓存击穿
是指缓存中没有但是数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读取缓存没有读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大。
2.2 出现的原因
系统中存在以下两个问题时需要引起注意:
- 当前key是一个热点key(例如一个秒杀活动),并发量非常大。
- 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次IO、多个依赖等。
在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃
2.3 解决方案
1.设置热点数据永不过期;
2.加互斥锁。
public static String getData(String key){
//从缓存读取数据
String result = getDataFromRedis(key);
//缓存中不存在数据
if(result == null ){
//去获取锁,获取成功,去数据库取数据
if(reenLock.tryLock()) {
//从数据库获取数据
result = getDataFromMysql(key);
//更新缓存数据
if(result != null ){
setDataToCache(key,result);
}
//释放锁
reenLock.unlock();
}
//获取锁失败
else{
//暂停100ms再重新去获取锁
Thread.sleep(100);
result= getData(key);
}
}
return result;
}
2种方案对比:
互斥锁:这种方案思路比较简单,但是存在一定的隐患,如果在查询数据库 + 和重建缓存(key失效后进行了大量的计算)时间过长,也可能会存在死锁和线程池阻塞的风险,高并发情景下吞吐量会大大降低!但是这种方法能够较好地降低后端存储负载,并在一致性上做得比较好。
“永远不过期”:这种方案由于没有设置真正的过期时间,实际上已经不存在热点key产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。
三、缓存雪崩
3.1什么是缓存雪崩
是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到而从数据库查。
3.2 出现的原因
缓存层由于某些原因不可用(宕机)或者大量缓存由于超时时间相同在同一时间段失效(大批key失效/热点数据失效),大量请求直接到达存储层,存储层压力过大导致系统雪崩
3.3解决方案
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的数据缓存数据库中;把缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务。利用sentinel或cluster实现。
- 设置热点数据永不过期。
- 采用多级缓存,本地进程作为一级缓存,redis作为二级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其他级别缓存兜底