redis的缓存穿透、缓存雪崩、缓存击穿

我们在处理缓存时,经常会遇到缓存满了、缓存数据的持久化、缓存穿透、缓存雪崩和缓存击穿。前两种用redis比较好解决:

缓存满了:我们可以使用redis(3.0以后)中的驱逐策略。

  • noeviction:不删除策略。达到最大缓存限制时,如果需要更多的内存,直接返回错误信息。大多数写命令都会导致占用更多的内存(有极少数会例外, 如 DEL )。

  • allkeys-lru:所有的key通用;有限删除出最近最少使用(less recently used,LRU)的key。

  • volatile-lru:只限于设置了expire的部分,优先删除最近最少使用的(less recently used,LRU)key。

  • allkeys-random:所有key通用,随机删除一部分key。

  • volatile-random:只限于设置了expire的部分,随机删除一部分key。

  • volatile-ttl:只限于设置了expire的部分;优先删除剩余时间(time to live,TTL)短的key。

缓存持久化:我们可以通过设置redis的RDB和AOF来实现缓存的落地。

接下来就要说本篇文章的重点了:缓存穿透、缓存雪崩、缓存击穿了

缓存穿透

首先我们知道缓存的作用是提高查询速度,查询请求来了先从缓存中查,如果没有的话,再去底层DB中查找,再添加到cache中。

什么是缓存穿透?

当你查询一个一定不存在的数据(cache和DB中都不存在)时,流程肯定是先去cache中查找,但是肯定查不到,然后去DB中查找,肯定也查不到,然后返回,但是DB中没有数据是不会缓存到cache中的。如果只是这么查询一次(低频率)看起来很正常,没有问题。但是如果有些人(黑客)恶意的用一定不存在的数据,高频率的查询,那么最终的结果是这些查询肯定都是走的DB查询,这样不仅我们的cache失去了存在的意义,还有可能把我们的DB挂掉,这就是一种隐藏的BUG。

解决缓存穿透

  • 第一种办法:简单粗暴,如果查询一定不存在的数据时,最后查询DB返回一个null值时,我们把这个null值也缓存到redis中,但是设置的超时时间短一些,这样当频繁查询这个不存在的数据时,我们就可以直接从cache中获得null值,而不用查询DB了。
  • 第二种办法:使用com.google.guava的布隆过滤器。将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层DB的查询压力。

缓存雪崩

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

解决缓存雪崩

解决方案可以跟解决缓存击穿的方案通用。我个人感觉缓存击穿就是一种特殊的缓存雪崩

缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。

缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮

解决缓存击穿

在说第一种方法之前我们先熟悉一下redis中的setnx命令的用法。因为我们第一种方法会用到这个命令。

SET key value

将key的值设置为value,当且仅当key不存在的时候。

若给定的key已经存在,setnx不会做任何操作。

SETNX 是【SET if Not eXists】(如果不存在,则SET)的缩写。

可用版本:>= 1.0.0

时间复杂度: O(1)

返回值: 设置成功,返回 1。设置失败,返回 0 

效果如下

redis> EXISTS job                # job 不存在
(integer) 0 
redis> SETNX job "programmer"    # job 设置成功
(integer) 1
redis> SETNX job "code-farmer"   # 尝试覆盖 job ,失败(integer) 0 
redis> GET job                   # 没有被覆盖"programmer"
  • 第一种方法:使用互斥锁

业界比较常用的做法。该方法是比较普遍的做法,即,在根据key获得的value值为空时,先锁上,再从数据库加载,加载完毕,释放锁。若其他线程发现获取锁失败,则睡眠50ms后重试。

String get(String key) {
     String value = redis.get(key);
     if (value  == null) {
      if (redis.setnx(key_mutex, "1")) {
          // 3 min timeout to avoid mutex holder crash
          redis.expire(key_mutex, 3 * 60);
          value = db.get(key);
          redis.set(key, value);
          redis.delete(key_mutex);
      } else {
          //其他线程休息50毫秒后重试
          Thread.sleep(50);
          get(key);
      }
    }
  }  

这种做法的优点:

  1. 思路简单
  2. 保证数据一致性

这种做法的缺点

  1. 代码复杂度增大
  2. 存在死锁的风险
  • 第二种方法:“永不过期”

这里的“永远不过期”包含两层意思: (1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现key同时集中过期和热点key过期问题(击穿问题和雪崩问题),也就是“物理”不过期。 (2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存放在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期 从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,出现数据不一致现象。

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, "1")) {
                          // 3 min timeout to avoid mutex holder crash
                          redis.expire(keyMutex, 3 * 60);
                          String dbValue = db.get(key);
                          redis.set(key, dbValue);
                          redis.delete(keyMutex);
                      }
                  }
              });
          }
          return value;
      } 

这种方法的优点

  1. 性价最佳,用户无需等待

这种方法的缺点

无法保证缓存一致性

你可能感兴趣的:(redis)