缓存穿透,缓存击穿,缓存雪崩是我们在应用缓存时最常碰到的问题,也是面试的热点考点。究竟什么是缓存穿透,缓存击穿,缓存雪崩,如何解决,本文会进行详细的剖析。
缓存穿透
什么是缓存穿透,我们通过一个例子来说明:
现在有这样一个需求需要根据文章id来查询文章信息,正常流程是先在Redis缓存中查找,如果找到了直接返回,找不到则到mysql数据库中查找,此时有两种情况:
1.数据库中查找到了将数据库中查找到的文章信息放入Redis缓存之后返回。
2.数据库中也没有查找到,这种情况下,每次查询该id的文章都会绕过Redis缓存直接到mysql数据库中查询,如果在短时间内发起大量查询可能导致数据库崩溃,这就是缓存穿透。
如何解决缓存穿透呢?
解决缓存穿透通常有两种方案:
第一种方案:缓存空数据
也就是虽然在数据库中查找不到该id的文章,也把该文章的信息缓存起来,只是缓存的为空值,这样以后每次查询这个id的文章就可以在缓存中命中,虽然值为空。
这种解决方案虽然可以解决数据库被压垮的问题,但是如果需要缓存的空值很多会导致内存消耗过大。
另外如果在数据库中已经插入了该记录而缓存中依然为空会导致缓存与数据库中的数据不一致。
第二种方案:使用布隆过滤器
从上图可以看出在查询缓存之前会先查询布隆过滤器,在布隆过滤器中判断该id的文章是否存在,如果存在继续查询Redis缓存,不存在则直接返回,这样请求就没有机会继续查询Redis缓存和mysql数据库。
究竟什么是布隆过滤器,它的工作原理又是什么呢?
布隆过滤器就是一个以位(bit)为单位的数组,数组中的每个元素只能存储二进制的0或者1,它的作用是检索一个元素是否在一个集合中。
布隆过滤器的工作原理
初始化位数组
初始状态下布隆过滤器中的元素都是0.
从数据库中查询出所有的key(即id)将这些key添加到布隆过滤器。
添加key时
使用多个hash函数对key进行hash运算得到一个整数值,对位数组长度进行取模运算得到一个位置,每个hash函数都会得到一个不同的位置,将这几个位置都置1就完成了添加操作。
判断是否存在
从布隆过滤器查询某个key是否存在时,先用添加key时的hash函数对这个key进行计算,查看布隆过滤器的位数组中对应的位置是否都为 1,只要有一个位为零,那么说明布隆过滤器中这个 key 不存在.如果这几个位置全都是 1,那么说明极有可能存在.因为这些位置的 1 可能是因为其他的 key 存在导致的。
可以看出使用布隆过滤器可以准确的判断出某key不存在,但是不能完全准确的判断其存在,有一定的误判率,不过该误判率对实际应用影响不大。
总结:使用布隆过滤器解决缓存穿透问题的优点是内存占用少,缺点是实现复杂而且存在一定概率的误判。
缓存击穿
什么是缓存击穿?
缓存击穿就是给缓存的某一个key设置了过期时间,当这个key过期而缓存重建还没有完成的时候有大量的请求过来,直接打在数据库上把数据库压垮。
有两种方案可以解决缓存击穿
第一种:互斥锁
如图所示,当线程一查询缓存没有命中的时候他会获取一个互斥锁,然后再去查询数据库重建缓存数据,此时线程二也来查询缓存当它获取互斥锁的时候失败于是等待重试直到线程一释放互斥锁线程二可以查询到缓存中的值。
通过加入互斥锁保证在缓存过期的时间段内只有一个线程能去访问数据库从而解决了缓存击穿。
这种方式虽然能够解决缓存击穿但是效率非常低,在一个线程重建缓存的过程中其它线程只能等待,再来看看第二种解决方案:
第二种:逻辑过期
我们将数据添加到缓存中去的时候不设置它的过期时间而是在缓存的数据中增加一个过期时间。
例如:{"id":"123","title":"今日暴雨","expire":153213455}
这里的"expire":153213455就是过期时间。这个过期时间不是真正的过期时间只是逻辑上的因此叫逻辑过期。
如图所示当线程1查询缓存的时候发现逻辑时间已过期,于是它获取互斥锁,之后它开启一个新的线程2,在线程2中查询数据库重建缓存,重置逻辑过期时间,一切做完以后在线程2中释放锁,而线程1在开启新线程以后立马返回过期数据。
在线程2重建缓存的过程中如果线程3也开始查询缓存,发现逻辑时间已过期,它会去获取互斥锁,因为此时互斥锁被线程2占用所以线程3获取锁失败直接返回过期数据。
这里的关键在于线程1和线程3都是直接返回的过期数据,从而避免了长时间的等待提高了程序的执行效率,但是做不到数据的强一致性。
缓存雪崩
缓存雪崩是指在同一时间段,大量缓存key同时失效或者Redis服务器宕机导致大量请求打到数据库,从而压垮数据库。
解决缓存雪崩通常有这样几种方案:
- 给不同key的过期时间添加随机值,避免这些key在同一时间同时失效。
- 利用Redis的集群提高缓存服务的可用性从而避免单机服务器宕机以后带来的影响。
- 给缓存业务添加限流策略避免大量的请求同时过来,可以通过nginx或网关来做到。