当前位置 博文首页 > 高亚峰的博客:缓存穿透、缓存击穿、缓存雪崩的问题与解决方案

    高亚峰的博客:缓存穿透、缓存击穿、缓存雪崩的问题与解决方案

    作者:[db:作者] 时间:2021-07-31 18:17

    缓存概念

    在计算器中,高速缓冲存储器是一个硬件或软件组件,其存储数据,以便该数据可以在将来的请求送达更快;存储在缓存中的数据可能是早期计算的结果,也可能是存储在其他位置的数据的副本。一个缓存命中时,所请求的数据在高速缓存中找到,而出现高速缓存未命中当它不能发生时发生。缓存命中是通过从缓存中读取数据来实现的,这比重新计算结果或从速度较慢的数据存储中读取要快。因此,从缓存中可以处理的请求越多,系统执行速度就越快。

    缓存穿透

    缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
    想象一下这个情况,如果传入的参数为-1,会是怎么样?这个-1,就是一定不存在的对象。就会每次都去查询数据库,而每次查询都是空,每次又都不会进行缓存。假如有恶意攻击,就可以利用这个漏洞,对数据库造成压力,甚至压垮数据库。即便是采用UUID,也是很容易找到一个不存在的KEY,进行攻击。
    (这里注意的区别:缓存穿透的概念与缓存击穿并不相同,下面会列出缓存击穿的概念)
    缓存穿透如何去预防呢?
    1)布隆过滤器

    布隆过滤器是一种数据结构,垃圾网站和正常网站加起来全世界据统计也有几十亿个。网警要过滤这些垃圾网站,总不能到数据库里面一个一个去比较吧,这就可以使用布隆过滤器。假设我们存储一亿个垃圾网站地址。

    可以先有一亿个二进制比特,然后网警用八个不同的随机数产生器(F1,F2, …,F8) 产生八个信息指纹(f1, f2, …, f8)。接下来用一个随机数产生器 G 把这八个信息指纹映射到 1 到1亿中的八个自然数 g1, g2, …,g8。最后把这八个位置的二进制全部设置为一。过程如下:
    在这里插入图片描述
    那这个布隆过滤器是如何解决redis中的缓存穿透呢?很简单首先也是对所有可能查询的参数以hash形式存储,当用户想要查询的时候,使用布隆过滤器发现不在集合中,就直接丢弃,不再进行缓存查询。
    2、缓存空对象

    当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;
    这种方法会存在两个问题(占用空间资源较多、业务一致性问题):

    Object nullValue = new Object();
    try {
      Object valueFromDB = getFromDB(uid); //从数据库中查询数据
      if (valueFromDB == null) {
        cache.set(uid, nullValue, 10);   //如果从数据库中查询到空值,就把空值写入缓存,设置较短的超时时间
      } else {
        cache.set(uid, valueFromDB, 1000);
      }
    } catch(Exception e) {
      cache.set(uid, nullValue, 10);
    }
    

    1.如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
    即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。(这个方法需要具体根据业务分析方案可行性。)

    缓存击穿

    缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。 缓存被“击穿”的问题,(这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key,这里是指同一个key。)
    缓存击穿的问题又该如何去预防呢?

    1. 数据持久化。设置热点数据永远不过期。(这个基于业务去考虑是否热点数据是可以持久化的操作。)
    2. 检查更新。将缓存key的过期时间(缓存数据存入时写的过期)一起保存到缓存中.在每次获取缓存数据操作后,都将get出来的缓存过期时间与当前系统时间做一个对比,如果缓存过期时间-当前系统时间<=1分钟(自定义的一个值),则主动更新缓存.这样就能保证缓存中的数据始终是最新的(和方案一一样,让数据不过期.)
    3. 使用互斥锁。
      互斥锁的实现思路是什么呢?就是当某一热点数据失效后,通过互斥锁的机制把所有并发实现一个串行化的管理。只有拿到锁的线程,才去数据库查询数据更新缓存,其他的线程在缓存中没有拿到数据且没有拿倒锁的状态下进行等待。这样很大程度下降低了对DB数据库的压力。不好的地方是可以当缓存失效时。会有短暂的等待时间。下面上一个互斥锁的demo。
      // 方法1:
    
    static Lock reenLock = new ReentrantLock();
     
        public List<String> getData() throws Exception {
            List<String> result = new ArrayList<String>();
            // 从缓存读取数据
            result = getCacheData();
            if (result.isEmpty()) {
                if (reenLock.tryLock()) {
                    try {
                        System.out.println("我拿到锁了,从DB获取数据库后写入缓存");
                        // 从数据库查询数据
                        result = getDataFromDB();
                        // 将查询到的数据写入缓存
                        setDataToCache(result);
                    } finally {
                        reenLock.unlock();// 释放锁
                    }
     
                } else {
                    result = getDataFromCache();// 先查一下缓存
                    if (result.isEmpty()) {
                        System.out.println("我没拿到锁,缓存也没数据,先进行等待");
                        Thread.sleep(100);// 等待
                        return getData04();// 重试
                    }
                }
            }
            return result;
          }
    

    缓存雪崩

    缓存雪崩概念是指:缓存层出现了错误,不能正常工作了。大量key同一时间点失效,同时又有大量请求打进来,导致流量直接打在DB上,造成DB也会CPU爆满挂掉的情况。
    那么如何解决缓存雪崩问题呢?

     redisTemplate.opsForValue().set("2","xxx",2, 随机数);
    
    解决思路1:可以给缓存的失效时间都设置成随机值,或者分析用户行为。利用程序手段尽量时缓存key值不在同一时间失效。从而减少DB的瞬间压力(根据具体业务分析)。
    解决思路2:设置多级缓存。比如同时使用redis和memcache缓存,请求->redis->memcache->db;这样可以方式redis缓存同一时间,并发请求不会直接打到DB问题,而是memcache缓存中那去数据。
    

    今天的分享到这里结束了。看完的小伙伴点个赞再走呗。

    cs