1.缓存穿透
缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。简单来说就是redis里面查不到,然后再数据库里面也没有查到。
造成缓存穿透的基本原因有两个:
第一, 自身业务代码或者数据出现问题,因为在高并发场景中,一些小的问题,可能会被无限的放大,造成不可损失的后果;
第二, 一些恶意攻击、大量恶意数据注入、 爬虫等造成大量空命中。
解决方案:
1.我们可以缓存一个空对象,我们第一次在数据库中获取的对象如果是空的,则我们可以对当前数据重新设置一个key和过期时间。
String get(String key) {
// 从缓存中获取数据
String cacheValue = cache.get(key);
// 缓存为空
if (StringUtils.isBlank(cacheValue)) {
// 从存储中获取
String storageValue = storage.get(key);
cache.set(key, storageValue);
// 如果存储数据为空, 需要设置一个过期时间(300秒)
if (storageValue == null) {
cache.expire(key, 60 * 5);
}
return storageValue;
} else {
// 缓存非空
return cacheValue;
}
}
2.使用布隆过滤器
我们可以引入redisson的依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
示例伪代码如下
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonBloomFilter {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
//构造Redisson
RedissonClient redisson = Redisson.create(config);
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小
bloomFilter.tryInit(100000000L,0.03);
//将star插入到布隆过滤器中
bloomFilter.add("star");
//判断下面号码是否在布隆过滤器中
System.out.println(bloomFilter.contains("star"));//true
System.out.println(bloomFilter.contains("star1"));//false
System.out.println(bloomFilter.contains("star2"));//false
}
}
使用布隆过滤器需要把所有数据提前放入布隆过滤器,并且在增加数据时也要往布隆过滤器里放,注意:布隆过滤器不能删除数据,如果要删除得重新初始化数据,布隆过滤器缓存过滤示例伪代码:
//初始化布隆过滤器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆过滤器:预计元素为100000000L,误差率为3%
bloomFilter.tryInit(100000000L,0.03);
//把所有数据存入布隆过滤器
void init() {
for (String key : keys) {
bloomFilter.put(key);
}
}
String get(String key) {
// 从布隆过滤器这一级缓存判断下key是否存在
Boolean exist = bloomFilter.contains(key);
if (!exist) {
return "";
}
// 从缓存中获取数据
String cacheValue = cache.get(key);
// 缓存为空
if (StringUtils.isBlank(cacheValue)) {
// 从存储中获取
String storageValue = storage.get(key);
cache.set(key, storageValue);
// 如果存储数据为空, 需要设置一个过期时间(300秒)
if (storageValue == null) {
cache.expire(key, 60 * 5);
}
return storageValue;
} else {
// 缓存非空
return cacheValue;
}
}
}
2.缓存击穿(失效)
由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大甚至挂掉。
简单来说就是可能在批量增加缓存的时候设置了同一个时间的过期时间,由于刚好在这个时间点缓存过期了,失效了,大量请求通过redis没有查到,直接到数据库里面查,然后导致我们的数据库压力过大甚至挂掉,所以对于这种情况我们在批量增加缓存时最好将这一批数据的缓存过期时间设置为一个时间段内的不同。
与缓存穿透的区别简单来说就是:缓存穿透是redis和数据库里面都没有查询数据,而且缓存击穿是redis里面没有,但是数据库存在。
String get (String key){
// 从缓存中获取数据
String cacheValue = cache.get(key);
// 缓存为空
if (StringUtils.isBlank(cacheValue)) {
// 从存储中获取
String storageValue = storage.get(key);
cache.set(key, storageValue);
//设置一个过期时间(360到720之间的一个随机数)
int expireTime = new Random().nextInt(360) + 360;
if (storageValue == null) {
cache.expire(key, expireTime);
}
return storageValue;
} else {
// 缓存非空
return cacheValue;
}
}
3.缓存雪崩
key对应的数据存在,但是极短时间内有大量的key集中过期,此时若有大量的并发请求过来,发现缓存 没有数据,大量的请求就会落到db上去加载数据,会将db击垮,导致服务奔溃。
缓存雪崩与缓存击穿的区别在于:前者是大量的key集中过期,而后者是某个热点key过期。
由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降),于是大量请求都会打到存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况。
解决方案:
(1)构建多级缓存
nginx缓存+redis缓存+其他缓存(ehcache等)
(2)使用锁或队列
用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上,不适用高并发情况。
(3)监控缓存过期,提前更新
监控缓存,发下缓存快过期了,提前对缓存进行更新。
(4)将缓存失效时间分散开
比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样缓存的过期时间重复率就会降低,就很难引发集体失效的事件。
上述就是缓存穿透,击穿,雪崩的具体区别以及对应策略,希望能帮助到各位小伙伴!