使用缓存时的三大经典问题,穿透、击穿、雪崩,今天我们就一起来讨论一下这三个常见问题出现的原因以及解决方案。
一、缓存穿透
1.1 概念
要查询的数据在缓存和数据库中都不存在,但是用户还是一直发请求查询这条数据,导致请求每次都会到数据库,如果有大量这样的请求过来,很容易就造成数据库宕机。
如果我们不处理这个问题,那么黑客就可以利用这个漏洞,召集大量肉鸡去请求不存在的数据,轻松搞垮你的网站。
1.2 解决方案
-
对不存在的数据设置空缓存
如果某条查询走数据库查询的结果还是空,那么我们可以为这条空数据做个空缓存,并设置一个较短的过期时间。
伪代码如下:
Object res = redis.get(key); //命中缓存,直接返回 if(null != res){ return res; } //缓存没有,查数据库 res = Db.get(key); if(null == res){//如果数据库中也没有数据,则在缓存中放一个空值,并设置一个较短的过期时间 res = new Object(); redis.set(key,res,1,TimeUnit.MINUTES); return; } //数据库有数据,放入缓存,并返回结果 redis.set(key,res); return res; -
布隆过滤器
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。布隆过滤器可以告诉我们
某样东西一定不存在或者可能存在。我们利用这一特性来解决缓存穿透的问题。布隆过滤器在Redis中的实现其实就是一个大型的位数组(二进制数组,只存0和1) + 多个无偏hash函数。伪代码
//判断要查询的数据是否存在 boolean res = filter.mightContain(id); if(!res){ //布隆过滤器判断不存在,那就一定不存在,直接返回 return; } //如果判断数据存在(有一定的误判概率),则先走缓存,缓存没有再走数据库.... ....
二、缓存击穿
2.1 概念
Redis中有一个热点key在过期的同时,有大量的请求过来,这些请求就会直接打到数据库,造成数据库宕机。
2.2 解决方案
-
互斥锁
在并发的情况下,使用互斥锁可以保证只放一个请求去查询数据库,查完数据库之后缓存中就有数据了,那么其他的请求就可以直接从缓存中拿到数据,从而避免了大量请求同时去查询数据库,造成数据库压力过大。
单机部署:
单机部署的情况下,直接使用synchronized本地锁即可解决。
伪代码
Object res = redis.get(key); if(res != null) return res; synchronized(this){ res = redis.get(key); if(null == res){ //走数据库查询 res = Db.get(key); //防止缓存穿透 if(res == null){ redis.set(key,res,1,TimeUnit.MINUTES); } } } return res;分布式部署:
分布式部署的情况下可以使用Redis来实现分布式锁,和单机模式的目的其实相同,也是保证只有一个请求去请求数据库。
Object res = redis.get(key); if(null != res ) return res; //缓存没结果,先拿到锁,再去请求数据库 boolean isLock = redission.lock(); if(isLock){ //拿到锁,再次判断缓存是否有数据,双重检查 res = redis.get(key); if(res == null){ res = Db.get(key); //防止缓存穿透 if(res == null){ redis.set(key,res,1,TimeUnit.MINUTES); } } } return res; -
设置热点Key永不过期(加逻辑过期时间)
对于某个需要频繁获取的信息,缓存在Redis中,不设置过期时间,但是在保存的内容中添加一个字段expire,存储当前的时间戳,每次获取数据的时候先对比一下存储的时间戳和当前时间戳之间相差的时间是否超出了我们期望的时间,如果超出了这个时间,获取互斥锁,新开一个线程,重新在数据库中获取数据,拿到最新数据再写入缓存中。这样这个热点Key相当于是可以一直存在,但是会有一小段时间缓存中存储是旧数据,一般影响不大。
-
定时更新
比如设置的热点key过期时间为1h,那么我们可以在59min的时候通过定时任务去更新这个热点key,并重新设置过期时间。
三、缓存雪崩
3.1 概念
Redis中的缓存数据大面积同时过期,或者Redis宕机,会导致大量的请求直接打到数据库,造成数据库宕机。
3.2 解决方案
-
过期时间 + 随机数
我们可以给缓存的过期时间后面再加上一个随机数,以保证各个key的过期时间均匀分布,避免大量key同时过期。
redis.set(key,res,3000L + RandomUtil.randomNumbers(5), TimeUnit.MICROSECONDS); -
保证Redis服务的高可用,搭建Redis集群 ,或者使用哨兵模式。
-
提高数据库的容灾能力,分库分表,读写分离。