什么是缓存击穿、缓存穿透、缓存雪崩 ?

一、缓存击穿


定义:大量的请求同时查询一个热点key时,此时这个key正好失效,就会导致大量的请求打到数据库上


特征:

  • 针对单个热点key

  • 缓存刚好过期时发生

  • 并发请求量大


方案一、互斥锁

加锁更新,比如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写入缓存,再返回给用户,这样后面的请求就可以从缓存中拿到数据了。


方案二

将过期时间组合写在value中,通过异步的方式不断的刷新过期时间,防止此类现象,不推荐使用


方案三、永不过期策略

  • 对热点key设置永不过期

  • 通过后台任务定期更新


个人不太推荐,因为可能因为某些活动的原因,每次的热点Key可能太一样,设置永久不过期可能会越来越占用redis内存


二、缓存穿透


定义:请求去查询一条记录,先查redis无,后查mysql无,都查询不到该条记录,但是请求每次都会打到数据库上面去,导致后台数据库压力暴增,这种现象我们称为缓存穿透,这个redis变成了一个摆设


特点:

  • 查询不存在的数据

  • 可能是恶意攻击

  • 对数据库压力大


缓存穿透可能有两种原因:

  1. 自身业务代码问题

  2. 恶意攻击,爬虫造成空命中


它主要有两种解决办法:

1、缓存空值/默认值:一种方式是在数据库不命中之后,把一个空对象或者默认值保存到缓存,之后再访问这个数据,就会从缓存中获取,这样就保护了数据库


缓存空值有两大问题:


1、缓存空污染(Cache Pollution)

缓存空值会导致缓存中存在大量的空值条目,这些条目占用了宝贵的缓存空间。随着时间的推移,缓存中的空值条目可能会越来越多,导致缓存的有效容量减少,影响其他有效数据的缓存命中率


示例

假设我们频繁查询不存在的用户ID,这些不存在的用户ID会被缓存为空值。缓存中的空值条目会逐渐增多,占用大量空间

public String getUserFromCache(String userId) {
    String cacheValue = jedis.get(userId);
    if (cacheValue != null) {
        return "null".equals(cacheValue) ? null : cacheValue;
    }

    // 模拟从数据库查询数据
    String dbValue = queryUserFromDatabase(userId);
    if (dbValue != null) {
        jedis.setex(userId, 300, dbValue); // 缓存有效数据
        return dbValue;
    } else {
        jedis.setex(userId, 60, "null"); // 缓存空值
    }

    return null;
}


解决方案

设置较短的空值缓存时间:为空值设置一个较短的过期时间,确保空值不会长时间占用缓存空间。定期清理过期的空值条目

public String getUserFromCache(String userId) {
    String cacheValue = jedis.get(userId);
    if (cacheValue != null) {
        return "null".equals(cacheValue) ? null : cacheValue;
    }

    // 模拟从数据库查询数据
    String dbValue = queryUserFromDatabase(userId);
    if (dbValue != null) {
        jedis.setex(userId, 300, dbValue); // 缓存有效数据,设置较长的过期时间
        return dbValue;
    } else {
        jedis.setex(userId, 10, "null"); // 缓存空值,设置较短的过期时间
    }

    return null;
}


2、缓存一致性问题(Cache Consistency Issues)

缓存空值可能会引入缓存一致性问题。如果数据库中原本不存在的数据在之后被添加,而缓存中仍然是空值,那么查询请求仍然会返回空值,而不是最新的数据。


示例

假设用户ID “123” 最初不存在于数据库中,所以我们缓存了一个空值。后来,用户ID “123” 被添加到数据库中,但由于缓存中仍然存在空值,查询请求仍然返回空值,而不是最新的用户数据。


解决方案

使用布隆过滤器:布隆过滤器可以用于快速判断一个元素是否可能存在。通过布隆过滤器,可以避免将空值缓存到缓存中,同时减少缓存污染的可能性

  import com.google.common.hash.BloomFilter;
  import com.google.common.hash.Funnels;
  import java.nio.charset.Charset;

  public class CacheSolution {
      private Jedis jedis;
      private BloomFilter<String> bloomFilter;

      public CacheSolution() {
          jedis = new Jedis("localhost", 6379);
          bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 100000, 0.01);
      }

      public String getUserFromCache(String userId) {
          if (!bloomFilter.mightContain(userId)) {
              return null; // 如果布隆过滤器判断不存在,直接返回空值
          }

          String cacheValue = jedis.get(userId);
          if (cacheValue != null) {
              return "null".equals(cacheValue) ? null : cacheValue;
          }

          // 模拟从数据库查询数据
          String dbValue = queryUserFromDatabase(userId);
          if (dbValue != null) {
              jedis.setex(userId, 300, dbValue); // 缓存有效数据
              bloomFilter.put(userId); // 将键加入布隆过滤器
              return dbValue;
          } else {
              // 不缓存空值
              bloomFilter.put(userId); // 将键加入布隆过滤器
          }

          return null;
      }

      private String queryUserFromDatabase(String userId) {
          // 模拟数据库查询逻辑
          return "user_data_" + userId;
      }
  }


布隆过滤器里会保存数据是否存在,如果判断数据不不能再,就不会访问存储


能说说布隆过滤器吗 ?

布隆过滤器,它是一个连续的数据结构,每个存储位存储都是一个bit,即 0或者 1, 来标识数据是否存在。存储数据的时时候,使用K个不同的哈希函数将这个变量映射为bit列表的的K个点,把它们置为1。

我们判断缓存key是否存在,同样,K个哈希函数,映射到bit列表上的K个点,判断是不是 1:

1、如果全不是1,那么key不存在;

2、如果都是1,也只是表示key可能存在。


布隆过滤器也有一些缺点:

1、 它在判断元素是否在集合中时是有一定错误几率,因为哈希算法有一定的碰撞的概率。

2、不支持删除元素。


两种解决方案的对比:


3、在接口层增加校验,不合法的参数直接返回。不相信任务调用方,根据自己提供的 API 接口规范来,作为被调用方,要考虑可能任何的参数传值


三、缓存雪崩


定义:指大量缓存key同时失效 或 缓存服务宕机,导致所有请求直接到达数据库的现象


特征:

  • 多个key同时失效

  • 系统负载急剧上升

  • 可能引发连锁故障


缓存雪崩是三大缓存问题里最严重的一种,我们来看看怎么预防和处理。

  • 提高缓存可用性

    • 1、集群部署:通过集群来提升缓存的可用性,可以利用Redis本身的Redis Cluster或者第三方集群方案如Codis等。

    • 2、持久化机制

    • 3、主从复制

  • 过期时间

    • 1、均匀过期:为了避免大量的缓存在同一时间过期,可以把不同的 key 过期时间随机生成,避免过期时间太过集中。

    • 2、热点数据永不过期。

  • 熔断降级

    • 1、服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,暂时停止业务服务访问缓存系统。

    • 2、服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的 fallback(退路)错误处理信息。

posted @ 2024-11-07 16:09  jock_javaEE  阅读(46)  评论(0)    收藏  举报