击穿
指的是在高并发的前提下,单个key恰巧在请求到来之前过期了,在缓存中查不到,去数据库查询,这样如果数据量不大或者并发不大的话是没有什么问题的。如果数据库数据量大并且是高并发的情况下那么就可能会造成数据库压力过大而崩溃。
解决方案:
方案一:
通过synchronized+双重检查机制:某个key只让一个线程查询,阻塞其它线程
在同步块中,继续判断检查,保证不存在,才去查DB。
代码实例:
private static volaite Object lockHelp=new Object();
public String getValue(String key){
String value=redis.get(key,String.class);
if(value=="null"||value==null||StringUtils.isBlank(value){
synchronized(lockHelp){
value=redis.get(key,String.class);
if(value=="null"||value==null||StringUtils.isBlank(value){
value=db.query(key);
redis.set(key,value,1000);
}
}
}
return value;
}
这种方式缺点也比较明显,会阻塞线程,效率不好。
方案二:
使用互斥锁(mutex key)
业界比较常用的做法,是使用mutex。
简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,
而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,
当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
具体流程大致是这样:
当并发来临时,多个客户端请求都来获取某个过期的key。
但因为redis是单进程单实例的,所以请求也是一个一个来的。
假设有A,B线程,它们每一个进来都首先get一下对应的key,看是否存在。
如果不存在,此时都执行SETNX,谁先执行的,谁将返回成功(也就相当于获得了一把互斥锁)。
得到锁的继续去获取数据库数据并加载到redis缓存中。
得不到锁的将重复这个流程。
示例代码:
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
if (redis.setnx(key_mutex, 1)==1 ) { //代表设置成功,也就是得到了锁
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
return value;
} else { //取不到锁,睡眠一会,继续重试此方法。
sleep(10);
get(key); //重试
}
} else {
return value;
}
}
但这种想一想,有没有问题? 如果获得锁的线程,在取数据期间,还未取回的中途,发生了故障,是不是意味着这个锁永久不会解除了?也就是,死锁了。
于是我们进一步,觉得应该给锁加一个超时时间。
示例代码:
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置该key,3min的超时,避免死锁问题
if (redis.setnx(key_mutex, 1,3 * 60) == 1) { //代表设置成功,也就是得到了锁
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
return value;
} else { //取不到锁,睡眠一会,继续重试此方法。
sleep(10);
get(key); //重试
}
} else {
return value;
}
}
我们似乎解决了死锁的问题,但带来了新的问题,假如说获得锁的进程获取db数据的时候,因为db网络情况不好,造成连接阻塞,过了给定的超时时间,但实际上并不是出了故障。
但其他的线程可不管,你超时了我就要重新抢锁,这样的话,将会有额外的线程进行去数据获取数据。
怎么解决呢? 只能多加一个额外的线程,用来专门监测获得锁的进程的健康状态,并定时进行锁时间的加长。
至此,技术讨论到这已经可以了,我们已经发现,实现可以实现,但是可以体会到它的复杂度。。
方案三:
设置value永不过期。
这种方式可以说是最可靠的,最安全的。
这种方式下,如果要保持数据最新不妨这么试试:
起个定时任务或者利用TimerTask 做定时,每隔一段时间对这些key进行数据库查询更新一次缓存,当然前提时不会给数据库造成压力过大(这个很重要)
穿透
一般是出现这种情况是因为恶意频繁查询才会对系统造成很大的问题: key缓存并且数据库不存在,所以每次查询都会查询数据库让数据进行无用的空查询,损耗系统资源不说,并发在极大情况的时候,还会导致数据库崩溃。
解决方案:
方案一:
可以使用布隆过滤器来阻挡请求,而我们也可以分为三种使用方式:
1、在客户端层面自行实现布隆过滤器,这样根本请求不用到达redis,就可以拦截,如果业务允许,应该是最有效的方式。
2、redis端实现布隆过滤器,借助使用bitmap来实现。
3、直接集成布隆过滤器的插件,最省事。
但布隆过滤器存在缺点:
1、会存在一定的误判率
2、对新增加的数据无法进行布隆过滤
3、布隆过滤器只能添加,不能删除,不能适应key的频繁修改。(这点可以参考一下布隆过滤器的升级-布谷鸟过滤器解决这个问题)
方案二:
将击透的key缓存起来,但是时间不能太长,下次进来是直接返回不存在,但是这种情况无法过滤掉动态的key,就是说每次请求进来都是不同key,这样还是会造成这个问题
雪崩
雪崩与击穿概念比较类似,但雪崩强调的是多个key查询并且出现高并发,缓存中失效或者查不到,然后都去db查询,从而导致db压力突然飙升,从而崩溃。
出现原因: 1、key同时失效。 2、 redis本身崩溃了
解决方案:
解决方向根据业务场景分为两种:
一、非固定时点批量key过期
1、对非固定时点必须失效的情景下,不同的key,设置不同随机的过期时间,具体值可以根据业务决定,让缓存失效的时间点尽量均匀。
二、固定时点key过期
1、在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。(跟击穿的第一个方案类似,但是这样是避免不了其它key去查数据库,只能减少查询的次数)。
2、可以通过缓存reload机制,预先去更新缓存,在即将发生大并发访问前手动触发加载缓存。(但此方法建立在业务允许,如果有些数据只能在固定时点后才能拿到,那么此方法不可行)
3、如果场景要求例如0点必须某批key必须全部失效,那么其实还可以在业务代码层面进行限制,在时点来临后的某个时间段,进行对请求随机延迟一下,这样尽可能的让延迟稍微长些的请求在请求key的时候可以直接在缓存已经可以取到了由延迟短些的已经从数据库取回的数据了。
redis作为分布式锁的可行性
我们在击穿的解决方案中,第二种其实就已经用到了redis分布式锁的概念了。
可以体会到,如果使用redis作为锁,大致要通过三点实现:
1、setnx
2、设置过期时间
3、额外的守护线程监听获得锁的进程,延长锁时间。
复杂度其实是有的。 当然,如果你想用,也有现成实现好的:redisson
但一般来说,如果到此就满足解决分布式锁的问题,那zookeeper的存在也就没有意义了。
我们使用锁,一般不会特别在乎效率的差异,但对一致性肯定有强烈的要求。
这一点上,zookeeper的特性使得它可以非常简单的API就能实现分布式锁。
虽然zookeeper肯定不如redis性能高,但是一致性上面,绝对是远远强于redis的。
zookeeper的分布式锁我们后续讲解。
参考文章:
https://blog.csdn.net/qq_27409289/article/details/85885121