这次给大家整理一篇Redis经常被问到的问题:缓存穿透、缓存雪崩、缓存预热、缓存更新、缓存降级等概念及简单解决方案。
一、缓存穿透
缓存穿透是指用户查询数据库没有的数据,缓存中自然也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求绕过缓存直接查数据库,缓存形同虚设,这也是经常提的缓存命中率问题。
有很多种方法可以有效地解决缓存穿透问题,最长见的有空对象和布隆过滤器两种解决方案。
空对象是首选方案,简单直接,碰到查询结果为空的键,放一个空值在缓存中,下次再访问就立刻知道这个键无效,不用发出SQL了。
但存在如下问题:
对于第一点,我还建议空值放在另外的缓存空间中,不宜与正常值共用空间,否则当空间不足时,缓存系统的LRU算法可能会先剔除正常值,再剔除空值——这个漏洞可能会受到攻击。
对于第二点,如果是Redis缓存,更新数据后直接在Redis中清除即可;如果是本地缓存,就需要用消息来通知其他机器清除各自的本地缓存了。
布隆过滤器。将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率(错误率可调)和删除困难。
二、缓存雪崩
可以理解为:由于原有缓存失效(过期),新缓存未到期间(如:采用了相同的缓存过期时间策略,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库,而对数据库CPU和内存造成巨大压力,甚至造成数据库宕机,从而引起一系列连锁反应,造成整个系统崩溃。
缓存正常从Redis中获取,示意图如下:
缓存失效瞬间示意图如下:
解决方案
1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
2:缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件
3:做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期
以下简单介绍两种实现方式的伪代码:
(1)碰到这种情况,一般并发量不是特别多的时候,使用最多的解决方案是加锁排队,伪代码如下:
加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!
注意:加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!
(2)还有一个解决方案是:给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存,实例伪代码如下:
解释说明:
1、缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;
2、缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。 这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。
关于缓存崩溃的解决方法,这里提出了三种方案:使用锁或队列、设置过期标志更新缓存、为key设置不同的缓存失效时间,还有一个被称为“二级缓存”的解决方法,有兴趣的读者可以自行研究。
三、缓存预热
缓存预热这个应该是一个比较常见的概念。新的缓存系统没有任何缓存数据,在缓存重建数据的过程中,系统性能和数据库负载都不太好,所以最好是在系统上线之前就把要缓存的热点数据加载到缓存中,这种缓存预加载手段就是缓存预热。
解决思路:
单机web系统情况下比较简单。
分布式缓存系统,如Memcached,Redis,比如缓存系统比较大,由十几台甚至几十台机器组成,这样预热会复杂一些。
缓存预热的目标就是在系统上线前,将数据加载到缓存中。
四、缓存更新
因为内存受限于空间缓存只能存储有限的数据,因此我们需要决定在我们的应用场景中,使用何种缓存更新策略,下面介绍几种常见的模式。
Cache-Aside模式
应用负责基于存储读写数据,缓存不直接和存储打交道,应用的行为如下:
Memcached通常被应用于这种方式,这种模式对于接下来的数据读取将非常快,Cache-Aside也叫做延迟加载,只有需要的数据被缓存,避免不需要的数据占用缓存空间。
这种模式的缺点如下:
Write-Though模式
应用将缓存作为主要存储,读写都直接和缓存打交道,缓存负责基于存储进行读写:
Write-Though对于所有的写操作都是比较慢的,但是对于读来说很快,用户通常需要容忍写延迟,但是不会出现脏数据。
这种模式的缺点如下:
Write-Behind模式
在这种模式下,应用的行为如:
这种模式的缺点如下:
Refresh-Ahead模式
我们可以配置缓存自动在最近访问的数据过期之前更新它们,如果可以准确预测将要访问的数据,Refresh-Ahead模式可以有效地减少读写的延迟。
这种模式的缺点如下:
五、缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹,而降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开。
六、总结
这些都是实际项目中可能碰到的一些问题,也是面试的时候经常会被问到的知识点,实际上还有很多各种各样的问题,文中的解决方案,也不可能满足所有的场景。一般正式的业务场景往往要复杂的多,应用场景不同,方法和解决方案也不同,具体解决方案要根据实际情况来确定!
由于篇长的原因有需要LeetCode刷题笔记+视频中源码资料+Java全栈开发学习路线图大家私信我回复“java教程”即可