悟纤:师傅,我前几天去面试,被面试官虐的不行。
师傅:是不是觉得面试官虐你千百遍,你还得待她为初恋。
悟纤:可不,我还不敢说啥,还得好好说话。
师傅:缓存穿透、缓存击穿、缓存雪崩,这些名词那是高大上上档次,但是实际实际很简单的。你要不会,待为师用我帅帅的姿势和你讲解下。
悟纤:师傅,那还不赶紧来。听完课还要出去浪呐!
一、缓存架构图
(1)在没有引入缓存的时候,我们请求的数据都是上数据库直接查询了。
(2)引入缓存之后,我们在获取数据时会先去缓存看看有没有缓存数据,有直接返回,没有上数据库进行查询,然后设置到缓存中,再进行返回。
(3)并不是所有的数据都要放到缓存中了:访问频率低的、读少写多的、一致性要求高的,这些就不要缓存了。
二、缓存穿透、击穿、雪崩
2.1 缓存穿透
师傅:纳尼,不是做缓存了,怎么还一直请求到数据库?
悟纤:你不会告诉我这就是缓存穿透吧?
师傅:是的,这就是缓存穿透造成的结果。
师傅:我们先来看下缓存穿透的定义吧,如下:
缓存穿透:在高并发下,查询一个不存在的值时,缓存不会被命中,导致大量请求直接落到数据库上,如活动系统里面查询一个不存在的活动。
师傅:这个名词取的多形象呐,缓存穿透。
师傅:你从何而来,要到到何处,从缓存过来,到数据库去。直接从缓存穿、透过去了,有点是绕开了缓存的意思。
悟纤:师傅这是一语道破天机呐。
师傅:简单说就是,
缓存穿透:请求未命中缓存,直接到数据库,这就是缓存穿透。比如:查询一个不存在的值的时候,如发起为id为“-1”的数据或id为特别大不存在的数据
师傅:明白了,一箭穿心,就是直接从缓存系统传过去,透到数据库,就是缓存穿透。
2.2 缓存击穿
师傅:理解了缓存击穿,接下来咱们来讲缓存击穿,注意这两个差别。
师傅:缓存穿透是查询一个不存在的数据,导致每次都打到数据库上了。缓存击穿,是确实在缓存有数据,我很用心的给你传了一个活动id = 666的。
悟纤:那怎么还会有问题呐?这不是很好嘛,每次请求都命中缓存了,使用率很高耶。
师傅:师傅,徒儿你还是太嫩了,还得在吃几年草。
悟纤:师傅,你又逗徒儿开心了,我不是你的小宝贝嘛。
师傅:(*^▽^*) , 你想想缓存是有失效时间的吧,那么如果缓存刚好到期了,而此时有一万个麒麟臂向你扑面而来,哦,不,是有大量请求过来,直接打到了数据库。看看定义吧:
缓存击穿:缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
师傅:一般这样的key都是热点key,玩过秒杀,秒杀中的key就是热点key。
悟纤:师傅我懂了,就是热点key在高并发下刚好过期了,这些秒级请求直接打到数据库,如果数据库处理不过来,那就挂了。
师傅:嗯嗯,很对,所以缓存击穿简单理解就是:
缓存击穿:热点key,缓存过期,直击数据库。比如:秒杀活动的时候,大量用户进行抢购某个商品,而此时刚好key过期了,那么这些请求就直接命中到数据库了。
师傅:我们在来从词理解下这个意思。你看缓存击穿,何谓击穿,不就是有一个东西顶着,然后没顶住,就穿过去了嘛。请求击倒缓存,缓存碰巧开小差了下,就穿到数据库了。
2.3 缓存雪崩
师傅:我们先理解下雪崩的意思,大量雪体崩塌,这种自然想象就是雪崩。
悟纤:这个我也知道,那和我们的缓存有什么关系呢。
师傅:你看,大量雪对应缓存中的就是大量的key,崩塌就是缓存过期失效了,这就造成缓存雪崩了,直接请求到数据库了,同样的我们看下定义。
缓存雪崩:大量的缓存key在同一时间失效,导致大量的请求落到数据库上,如活动系统里面同时进行着非常多的活动,但是在某个时间点所有的活动缓存全部过期。
BTW:击穿与雪崩的区别即在于击穿是对于某一特定的热点数据来说,而雪崩是全部数据。
三、缓存穿透、击穿、雪崩进阶理解
3.1 理解一
师傅:徒儿,你看我们上面介绍的定义,发现什么规律没有?
悟纤:递进关系。
师傅:也有点那么点关系,上面在介绍的是从无到有,再到多的一种方式。
师傅:来,跟上师傅的思路,当我们正常使用缓存的时候,设置key,获取key。这个使用起来不会有啥问题,但是攻击者不乖哦,就不按照正常路子来,要么不来,要么就多来了。
悟纤:师傅,你把我都整蒙了,请hello world的语言和我说下好不。
师傅:当“无key”的时候,也就是key不存在,这时候频繁请求,就会越过缓存,造成缓存穿透。
师傅:当“有一key”且很“火热”的时候,也就是热点key,这时候频繁请求,碰巧缓存过期了,就会造成缓存击穿。
师傅:当“有多key”的时候,也就是说大量的key,在同一时间失效了,欧侯,这时候就都请求到数据库了,这就会造成缓存雪崩了。
3.2 理解二
我们也可以这么理解,上面缓存的三种情况,就是在缓存在高并发下的并发问题。
高并发下,不停的访问一个不存在的key,造成请求绕过了缓存系统,请求落到了数据库,就造成了缓存穿透。
高并发下,不停的访问一个热点key,在缓存失效的时候,请求在此时落到了数据库,就造成了缓存击穿(缓存击穿,本来是有人挡着的,但是利用了缓存失效的间隙,进行了攻击)。
高并发下,大批量缓存的key同时失效,此时所有请求落到了数据库,造成数据库压力过大,这就是缓存雪崩。
四、缓存穿透、击穿、雪崩如何解决?
4.1 缓存穿透如何解决?
(1)在接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接return。
(2)缓存NULL值,但是缓存NULL的时间不能太长,否则NULL数据长时间得不到更新,也不能太短,否则达不到防止缓存击穿的效果。
(3)布隆过滤器(Bloom Filter):类似于哈希表的一种算法,用所有可能的查询条件生成一个bitmap,在进行数据库查询之前会使用这个bitmap进行过滤,如果不在其中则直接过滤,从而减轻数据库层面的压力。
4.2 缓存击穿如何解决?
(1)设置热点数据永远不过期。
(2)使用加互斥锁:对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询。
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//假设设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
4.3 缓存雪崩如何解决?
(1)缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
(2)设置热点数据永远不过期。
五、悟纤小结
悟纤:师傅,你是在太牛逼了,终于知道你为哈要剃个光头了 —— 聪明绝顶了。
师傅:几天没打你,你这是又皮痒了吧。
悟纤:师傅,徒儿不敢,徒儿这本意是要表达对师傅的培养,不想词穷…
师傅:就你这机灵的脑瓜,如果用在学习上,你早就超过师傅了。
悟纤:师傅说的是,那我给师傅总结下今天讲的内容吧,以表我认真学习的决心。
(1)缓存穿透:查询一个不存在的值时,未命中缓存,直接落到了数据库。解决方案:接口校验、缓存NULL值、Bloom Filter。
(2)缓存击穿:热点key,缓存过期,直击数据库。解决方案:设置永不过期、加锁互斥获取数据。
(3)缓存雪崩:大量的缓存key在同一时间失效,导致大量请求落到数据库上。解决方案:设置用不过期、随机过期时间。