Redis 实战 - 缓存异常及解决方案

在这里插入图片描述

把今天最好的表现当作明天最新的起点…….~

概述

  在实际的业务场景中,Redis 一般和其他数据库搭配使用,比如和关系型数据库 MySQL 配合使用,用来减轻后端数据库的压力。Redis 会把 MySQL 中经常被查询的数据缓存起来,比如热点数据,这样当用户来访问的时候,就不需要到 MySQL 中去查询,而是直接获取 Redis 中的缓存数据,从而降低了后端数据库的读取压力。如果说用户查询的数据在 Redis 没有找到,那么用户的查询请求就会被转到 MySQL 数据库。当 MySQL 将查询到的数据返回给客户端时,同时也会将数据缓存到 Redis 中,这样用户再次读取时,就可以直接从 Redis 中获取数据。流程图如下所示:
在这里插入图片描述
  在使用 Redis 作为缓存数据库的过程中,有时也会遇到一些棘手问题,比如常见缓存穿透、缓存击穿和缓存雪崩等问题,如下图所示。本文中将对这些问题做简单地说明,并且提供有效的解决方案。

Redis 缓存异常
缓存穿透
缓存击穿
缓存雪崩

一、缓存穿透

1.1 缓存穿透是什么

  一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,那么就去数据库去查找。当用户查询某个数据时,Redis 中不存在该数据,也就是缓存没有命中,此时查询请求就会转向数据库,结果发现数据库中也不存在该数据,数据库只能返回一个空对象(相当于进行了两次无用的查询)。用户拿不到数据时,就会一直发请求查询数据库,这样会对数据库的访问造成很大的压力。如果这种类请求非常多,或者用户利用这种请求进行恶意攻击,就会给数据库造成很大压力,甚至于崩溃,这种现象就叫缓存穿透。
在这里插入图片描述

  这种现象的原因其实很好理解,当客户端访问不存在的数据时,先请求 Redis,但是此时 Redis 中并没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库。我们都知道数据库能够承载的并发不如 Redis 这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库。

1.2 解决方案

  简单的解决方案就是当Redis、数据库中都没有值返回空对象时, 可以在 Redis 中存放一个空值,同时为其设置一个过期时间。这样,当用户再次发起相同请求访问这个不存在的数据,那么就会从缓存中拿到一个空对象,用户的请求被阻断在了缓存层。这样就可以减少重复查询空值引起的系统压力增大,从而从而保护了后端数据库。示例代码如下:

private String queryMessager(String key){
    // 从缓存中获取数据
    String message = getFromCache(key);
    // 如果缓存中没有 从数据库中查找
    if(StringUtils.isBlank(message)){
        message = getFromDb(key);
        // 如果数据库中也没有数据 就设置短时间的缓存
        if(StringUtils.isBlank(message)){
            // 设置缓存时间(缓存的key,缓存的值,失效时间:单位秒)
            redisClient.setNxEx(key,null,60);
        } else {
            redisClient.setNxEx(key,message,1800);
        }
    }
    return message;
}

  这种做法虽然优化了缓存穿透问题,但也存在一些问题。虽然请求进不了数据库,但是会占用 Redis 的缓存空间。而大量的空缓存导致资源的浪费,也有可能导致 Redis 和数据库中的数据不一致。

二、缓存击穿

2.1 缓存击穿是什么

  我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据。比如某个热点数据,它无时无刻都在接受大量的并发访问,如果在某一时刻忽然过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,导致大量的并发请求直接访问数据库,就像在一个完好无损的桶上凿开了一个洞,引起数据库压力瞬间增大,这种现象被称为缓存击穿。
在这里插入图片描述

  缓存击穿一般出现在高并发系统中,是大量并发用户同时请求到缓存中没有但数据库中有的数据,也就是同时读缓存没读到数据,又同时去数据库去取数据。由于请大量请求同时过来,来不及更新缓存就全部打到数据库那边,引起数据库压力瞬间增大。

2.2 解决方案

  • 将热点数据设置加上互斥锁
    • 此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。当第一个数据库查询请求发起后,就将缓存中该数据上锁;此时到达缓存的其他查询请求将无法查询该字段,从而被阻塞等待;当第一个请求完成数据库查询,并将数据更新值缓存后,释放锁;此时其他被阻塞的查询请求将可以直接从缓存中查到该数据。

      private ReentrantLock reentrantLock = new ReentrantLock();
      public static String getData(String key) throws InterruptedException {
          // 从 Redis 查询数据
          String result = getDataByKey(key);
          // 参数校验
          if (StringUtils.isBlank(result)) {
              // 获取锁
              if (reentrantLock.tryLock()) {
                  // 去数据库查询
                  result = getDataByDB(key);
                  // 校验
                  if (StringUtils.isNotBlank(result)) {
                      // 搞进缓存
                      setDataToKey(key, result);
                  }
                  // 释放锁,正常会在finally中释放
                  reentrantLock.unlock();
              } else {
                  // 稍等一下
                  Thread.sleep(100L);
                  result = getData(key);
              }
          }
          return result;
      }
      
    • 当某一个热点数据失效后,只有第一个数据库查询请求发往数据库,其余所有的查询请求均被阻塞,从而保护了数据库。但是,由于采用了互斥锁,其他请求将会阻塞等待,可能会存在死锁和线程池阻塞的风险,此时系统的吞吐量将会下降,这需要结合实际的业务考虑是否允许这么做。

  • 将热点数据设置为永远不过期
    • 当向缓存中存储这些数据的时候,可以将他们的缓存失效时间错开,这样能够避免同时失效。如在一个基础时间上加/减一个随机数,从而将这些缓存的失效时间错开。

      private void setRandomTimeForReidsKey(String redisKey,String value){
          //随机函数
          Random rand = new Random();
          //随机获取30分钟内(30*60)的随机数
          int times = rand.nextInt(1800);
          //设置缓存时间(缓存的key,缓存的值,失效时间:单位秒)
          redisClient.setNxEx(redisKey,value,times);
      }
      
    • 这种方案由于没有设置真正的过期时间,实际上已经不存在热点key产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。

三、缓存雪崩

3.1 缓存雪崩是什么

  通常,为了保证 Redis 中的数据与数据库中的数据一致性,通常会给 Redis 里的数据设置过期时间。当缓存数据过期后,用户访问的数据如果不在 Redis 里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。

应用 Redis 数据库 从缓存读取数据 缓存过期 缓存不存在 从数据库读取数据 返回数据库中的数据 将数据加载到缓存 应用 Redis 数据库

  但当Redis 故障宕机或者缓存中大批量的数据同一时间过期(失效),而此时数据访问量又非常大,无法在 Redis 中处理,于是全部直接访问数据库,从而导致数据库压力突然暴增,严重时甚至可能导致数据库崩溃。就像雪崩一样,引发一系列连锁效应,从而波及整个系统崩溃,这种现象被称为缓存雪崩。如下图所示:
在这里插入图片描述

  假设当时每秒6000个请求,本来缓存在可以扛住每秒5000个请求,但是缓存当时所有的Key都失效了。此时1秒6000个请求全部落数据库,数据库必然扛不住,可能DBA都没反应过来就直接挂了,即便是重启数据库,但是数据库立马又被新的流量给打死了。以秒杀系统为例,图示说明:
在这里插入图片描述

  它和缓存击穿不同,缓存击穿是在并发量特别大时,某一个热点 key 突然过期,而缓存雪崩则是大量的 key 同时过期,因此它们根本不是一个量级。

3.2 解决方案

  出现上述情况的常见原因主要有以下两点:

  • 大量缓存数据同时过期,导致本应请求到缓存的需重新从数据库中获取数据。
  • Redis 本身出现故障,无法处理请求,那自然会再请求到数据库那里。

  针对上面出现故障的情况,可以从以下几点出发解决:

  • 事前:构建高可用的集群,实现主 Redis 实例挂掉后,能有其他从库快速切换为主库,继续提供服务,避免全盘崩溃。
  • 事中:在往 Redis 存数据时,可以通过随机、微调、均匀设置等方式设置过期时间,这样可以保证数据不会在同一时间大面积失效。如果事情已经发生了,那就要为了防止数据库被大量的请求搞崩溃,可以采用服务熔断或者请求限流的方法。当然服务熔断相对粗暴一些,停止服务直到redis服务恢复;而请求限流相对温和一些,保证一些请求可以处理,不过还是看具体业务情况选择合适的处理方案。
  • 事后:redis持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

四、拓展

4.1 缓存预热

  缓存预热就是系统上线前后,将相关的缓存数据直接加载到缓存系统中去,而不依赖用户。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户直接查询事先被预热的缓存数据,这样可以避免那么系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。根据数据不同量级,可以有以下几种做法:

  • 数据量不大:项目启动的时候自动进行加载。
  • 数据量较大:后台定时刷新缓存。
  • 数据量极大:只针对热点数据进行预加载缓存操作。

4.2 缓存降级

  缓存降级是指当缓存失效或缓存服务出现问题时,为了防止缓存服务故障,导致数据库跟着一起发生雪崩问题,所以也不去访问数据库,但因为一些原因,仍然想要保证服务还是基本可用的,虽然肯定会是有损服务。因此,对于不重要的缓存数据,我们可以采取服务降级策略。一般做法有以下两种:

  • 直接访问内存部分的数据缓存。
  • 直接返回系统设置的默认值。

五、结语

  Redis 缓存异常会面临的三个问题:缓存雪崩、击穿和穿透。其中,缓存雪崩和缓存击穿主要原因是数据不在缓存中,而导致大量请求访问了数据库,数据库压力骤增,容易引发一系列连锁反应,导致系统奔溃。不过,一旦数据被重新加载回缓存,应用又可以从缓存快速读取数据,不再继续访问数据库,数据库的压力也会瞬间降下来。因此,缓存雪崩和缓存击穿应对的方案比较类似。而缓存穿透主要原因是数据既不在缓存也不在数据库中。因此,缓存穿透与缓存雪崩、击穿应对的方案不太一样。
  Redis 缓存在互联网中至关重要,可以很大的提升系统效率。 本文介绍的缓存异常以及解决思路有可能不够全面,但也提供相应的解决思路和代码大体实现,希望可以为大家提供一些遇到缓存问题时的解决思路。如果有不足的地方,也请帮忙指出,大家共同进步。
在这里插入图片描述

  • 27
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

独泪了无痕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值