05-Redis的雪崩、击穿、穿透

一 穿透优化

1.1 缓存穿透概念

       缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,通常出于容错的考虑,如果从存储层拿不到数据就不写入缓存层。整个过程是:

  1. 缓存层没有命中
  2. 存储层没有命中,不将空结果写入缓存
  3. 返回空结果

       缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储层的意义。缓存穿透问题可能会使后端存储负载加大,由于很多后端存储不具备高并发性,甚至可能造成后端存储宕机。通常可以在程序中分别统计总调用数、缓存层命中数、存储层命中数,如果发现大量存储层空命中,可能就是出现缓存穿透问题。

1.2 缓存穿透原因

    造成缓存穿透的基本原因:

  • 自身业务代码或者数据出现问题
  • 恶意攻击、爬虫等造成大量命中

1.3 如何解决缓存穿透问题

  • 缓存空对象

       缓存空对象有两个问题:

       第一,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除;

       第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。比如,过期时间是5分钟,此时存储层添加了这个数据,那这段时间就会出现缓存层和存储层的数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。

  • 布隆过滤器拦截

       将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。 (https://blog.csdn.net/jack1liu/article/details/107135903

二 雪崩优化

2.1 雪崩概念

       缓存层由于某些原因不能提供服务,所有的请求都达到存储层,存储层的压力暴增,造成存储层宕机的情况。

2.2 解决方案

  • 情况一:我们设置缓存时采用了相同的过期时间

     解决:

  1. 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。
  2. 不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
  3. 做二级缓存,A1 为原始缓存,A2 为拷贝缓存,A1 失效时,可以访问 A2,A1 缓存失效时间设置为短期,A2 设置为长期。
  • 情况二:缓存服务器宕机

     解决:

  1. 保证缓存层服务高可用性
  2. 依赖隔离组件为后端限流并降级
  3. 提前演练

三 击穿优化

3.1 缓存击穿概念

  • 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
  • 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次 IO、多个依赖等。

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

       缓存击穿针对的是某一个热 key ;缓存雪崩针对的是很多 key。

3.2 解决方案

       我们的目标是:尽量少的线程构建缓存(甚至是一个) + 数据一致性 + 较少的潜在危险,

  • 使用互斥锁(mutex key):

       这种解决方案思路比较简单,就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据即可。

public String get(key) {  
      String value = redis.get(key);  
      if (value == null) { //代表缓存值过期  
          //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db  
          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并回设到缓存了,这时候重试获取缓存值即可  
                      Thread.sleep(50);  
                      get(key);  //重试  
              }  
          } else {  
              return value;        
          }  
 }  
  • 永远不过期

    这里的“永远不过期”包含两层意思:

    (1) 从 redis 上看,确实没有设置过期时间,这就保证了,不会出现热点 key 过期问题,也就是“物理”不过期。

    (2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在 key 对应的 value 里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期。

   两种方案的比较:

  • 互斥锁:方案思路简单,存在一定的隐患,如果构建缓存过程中出现问题或者时间较长,可能会出现死锁或者线程池阻塞风险,但是这种方法能够较好的降低后端存储负载,并在一致性上做的比较好。
  • 永远不过期:这种方案没有设置过期时间,实际上不存在热点 key 产生的一系列危害,会出现数据不一致的情况,同时代码复杂度会增加。

   参考文档

    http://download.redis.io/redis-stable/redis.conf

猜你喜欢

转载自blog.csdn.net/jack1liu/article/details/107217161