Redis缓存穿透、缓存击穿、缓存雪崩及其多种处理方式详解

文章目录

  • 一、缓存穿透(数据查不到)
    • 1.概念
    • 2.解决方案
      • (1)解决方案一:缓存空对象
      • (2)解决方案二:布隆过滤器
  • 二、缓存击穿(访问量太大,缓存正好过期)
    • 1.概述
    • 2.解决方案
      • (1)解决方案一:永不过期
      • (2)解决方案二:加互斥锁
      • (3)解决方案三:异步重建缓存——“提前“互斥锁
  • 三、缓存雪崩(缓存集体过期)
    • 1.概述
    • 2.解决方案
      • (1)解决方案一:高可用
      • (2)解决方案二:限流降级
      • (3)解决方案三:数据预热
      • (4)解决方案四:优化缓存过期时间
      • (5)解决方案五:过期时间处理

一、缓存穿透(数据查不到)

1.概念

        当用户想要查询一个数据时,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多时,缓存都没有命中,于是都去请求了持久层数据库。缓存本就是为了减轻数据库压力,这样redis形同虚设,这给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
img

2.解决方案

(1)解决方案一:缓存空对象

        当存储层没有查询到后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间(这里缓存不存在key的时候一定要设置过期时间,不然当数据库已经新增了这一条记录的时候,会导致缓存和数据库不一致的情况),之后再访问这个数据将会从缓存中获取,若我为空,就不会再去访问持久层数据库,保护了后端数据源,防止了攻击用户反复用同一个id暴力攻击。

Redis缓存穿透、缓存击穿、缓存雪崩及其多种处理方式详解_第1张图片
但有两种问题:

  1. 如果空值被缓存起来,这就意味着缓存需要更多的空间存储更多的键
  2. 即使对空值设置了过期时间,还是会在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

(2)解决方案二:布隆过滤器

        布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,同时引入一种节省空间的数据结构,位图(bitmap),他是一个有序的数组,只有两个值,0 和 1。0代表不存在,1代表存在。在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力。
Redis缓存穿透、缓存击穿、缓存雪崩及其多种处理方式详解_第2张图片
        事实上布隆过滤器并不是百分百过滤的,由于哈希冲突与位图特性,会出现误判情况,即将不存在的通过计算,误判为存在,这种情况被称为假阳性(False Positive Probability,FPP),但其出现的机率很小,默认为0.03,所以布隆过滤器不是万能的,但是它能帮我们抵挡掉大部分不存在的数据已经很不错了,已经减轻数据库很多压力了。

从容器的角度来说:

​ 如果布隆过滤器判断元素在集合中存在,不一定存在

​ 如果布隆过滤器判断不存在,一定不存在

从元素的角度来说:

​ 如果元素实际存在,布隆过滤器一定判断存在

​ 如果元素实际不存在,布隆过滤器可能判断存在

二、缓存击穿(访问量太大,缓存正好过期)

1.概述

        这里需要注意和缓存击穿的区别,是指一个key非常热点,在不停扛着高并发,高并发集中对这一个点进行访问,当这个key在过期的瞬间,持续的高并发就会穿破缓存,直接请求数据库,就像在一个屏障上凿了一个洞。
        当某个key在过期的瞬间,就会有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导致数据库瞬间压力增大

2.解决方案

(1)解决方案一:永不过期

        顾名思义,将key设置为永不过期,这样就不会出现缓存过期的情况。但是随着永久的key越来越多,可能对服务器的压力也会不断增大。

优点:不会出现缓存击穿
缺点:key逐渐增多,如果不常使用,会造成服务器资源浪费,增大服务器压力。

(2)解决方案二:加互斥锁

        业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去查询持久层数据库,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX,也就是只有不存在的时候才设置,可以利用它来实现锁的效果)去set一个mutex key,当操作返回成功时,再进行查询持久层数据库的操作并回设缓存。否则,就重试整个get缓存的方法。
        用浅显易懂的语言来说,就是如果有多个线程同时去访问缓存中同一个key,但是这个key并不存在,所以使用setnx先设置一个key,设置成功的那个线程然后就去持久层数据库拿数据,而其他线程使用setnx再设置这个key时,其已经存在,就会设置失败而被拦截,让他过一会再重新get这个key。等到那个线程从持久层数据库拿来值,并把key-value放入缓存中后,其他线程就能直接获取到缓存中正确的key。

代码如下:

public String get(key) { 
    String value = redis.get(key);
    	if (value == null) { // 缓存已过期
            //对其设置3分钟过期时间防止delete失败导致下次过期无法使用
        	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; 
        }
    } 
 }

优点:思路简单,保持一致性
缺点:代码更加复杂,存在死锁风险,存在线程池阻塞风险

(3)解决方案三:异步重建缓存——“提前“互斥锁

        我们也可以把过期时间存在key对应的value里,其值比真实过期时间早一些,如果发现要过期了 通过一个后台的异步线程对缓存进行重新构建。 其与永不过期类似,但若该key一直没有访问,就会正常过期,不会占用内存,如果高并发访问,也能一直维持不过期。

代码如下:

String get(final String key) {  
        V v = redis.get(key);  
        String value = v.getValue();  
        long timeout = v.getTimeout();  
        if (v.timeout <= System.currentTimeMillis()) {  
            // 异步更新后台异常执行  
            threadPool.execute(new Runnable() {  
                public void run() {  
                    String keyMutex = "mutex:" + key;  
                    if (redis.setnx(keyMutex, 13 * 60)) {  
                        // 对其设置3分钟过期时间防止delete失败导致下次过期无法使用
                        String dbValue = db.get(key);  
                        redis.set(key, dbValue);  
                        redis.delete(keyMutex);  
                    }  
                }  
            });  
        }  
        return value;  
}

优点:异步构建缓存,不会阻塞线程池
缺点:不保证一致性,非构建缓存线程可能访问老数据,代码复杂度增大且占用一定的内存空间(每个value都要维护一个timekey)

三、缓存雪崩(缓存集体过期)

1.概述

        缓存雪崩,是指在某一个时间段,缓存集中过期失效,导致请求全部转发到数据库,使数据库瞬间压力过大而导致雪崩。但这并不是最致命的,最致命的是断电断网或Redis服务器宕机。这对服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮,当然这是突发情况,只能通过高可用来解决,服务器宕机后,立即让其他服务器接替,而不影响正常业务的开展。

2.解决方案

(1)解决方案一:高可用

        既然redis有可能挂掉,那就增设几台redis,在多个机房部署redis,保证redis的高可用,使用主从+哨兵模式,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群,保证缓存层的高可用。

(2)解决方案二:限流降级

        限流的思想是,缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

(3)解决方案三:数据预热

​ 数据加热的含义就是在正式部署之前,我先把可能的数据先访问一遍,这样部分可能大量访问的数据就会加载在缓存中。在即将发生大并发访问前手动触发加载缓存不同的key。

(4)解决方案四:优化缓存过期时间

​ 比如可以将key过期时间设置不同的周期,再加上一些随机因子。这样尽可能分散缓存过期时间,防止大量key在同一时间过期。也可以将热门的key缓存时间长一些,冷门的key缓存时间短一些,也能节省缓存服务的资源。

(5)解决方案五:过期时间处理

  1. 设置key永不过期

  2. 使用互斥锁重建缓存

  3. 使用异步重建缓存

    PS:详细方法见上面缓存击穿对应处理方法

参考文献:
热点key问题:https://www.iteye.com/blog/carlosfu-2269687
熔断、限流、降级:https://zhuanlan.zhihu.com/p/61363959
布隆过滤器详解:https://blog.csdn.net/wuzhiwei549/article/details/

你可能感兴趣的:(Redis,数据库,Java进阶,redis,缓存,java)