关于缓存击穿、缓存穿透、缓存雪崩及解决方案

缓存在应用中的处理流程

前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时再从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果

在这里插入图片描述
看似简单的处理流程;然而在一个分布式应用中,只要是引入了缓存,为了保证系统的健壮性,在缓存系统设计时,就必须要考虑缓存击穿,缓存穿透,缓存雪崩的问题及解决方案

缓存击穿

缓存击穿:缓存中没有数据,但数据库中有数据。(一般是缓存时间到期导致缓存失效)这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力,甚至可能压垮数据库

在这里插入图片描述

解决方案

  • 设置热点数据不过期:直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。这种方式适用于比较极端的场景,例如流量特别特别大的场景,使用时需要考虑业务能接受数据不一致的时间,还有就是异常情况的处理,不要到时候缓存刷新不上,一直是脏数据,那就凉了
  • 加互斥锁:在并发的多个请求中,只有第一个请求(或前几个请求)线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,后面的请求直接从缓存中取数据。对于互斥锁的选择,可以是 Java API 层面的 Lock 或是 JVMsynchronized 或是分布式锁,只要保证数据库的请求能大大降低即可。注意加锁的维度要按 key 维度去加锁

使用分布式锁的伪代码,仅供参考

public Object getData(String key) throws InterruptedException {
    
    

    Object value = redis.get(key);
    // 缓存值过期
    if (value == null) {
    
    
        // lockRedis:专门用于加锁的redis;
        // "empty":加锁的值随便设置都可以
        if (lockRedis.set(key, "empty", "PX", lockExpire, "NX")) {
    
    
            try {
    
    
                // 查询数据库,并写到缓存,让其他线程可以直接走缓存
                value = getDataFromDb(key);
                redis.set(key, value, "PX", expire);
            } catch (Exception e) {
    
    
                // 异常处理
            } finally {
    
    
                // 释放锁
                lockRedis.delete(key);
            }
        } else {
    
    
            // sleep50ms后,进行重试
            Thread.sleep(50);
            return getData(key);
        }
    }
    return value;
}

缓存穿透

缓存穿透:查询一个数据,发现缓存中没有;于是查询数据库,发现也没有。而后面用户不断发起请求,缓存没有命中,于是都去请求了数据库。这给数据库造成很大的压力,引起数据库压力瞬间增大,造成过大压力,甚至可能压垮数据库

在这里插入图片描述

解决方案

  • 接口校验:在正常业务流程中可能会存在少量访问不存在 key 的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如用户鉴权校验,id 做基础校验,id<=0 的直接拦截
  • 缓存空值:从缓存取不到数据,在数据库中也没有取到,这时也可以将 key-value 对写为 key-null,缓存有效时间可以设置短点,如 30 秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个 id 暴力攻击
  • 布隆过滤器:使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库

布隆过滤器

布隆过滤器的特点是判断不存在的,则一定不存在;判断存在的,大概率存在,但也有小概率不存在。并且这个概率是可控的,我们可以让这个概率变小或者变高,取决于用户本身的需求

布隆过滤器由一个 bitSet 和 一组 Hash 函数(算法)组成,是一种空间效率极高的概率型算法和数据结构,主要用来判断一个元素是否在集合中存在

在初始化时,bitSet 的每一位被初始化为 0,同时会定义 Hash 函数,例如有 3Hash 函数:hash1、hash2、hash3

写入流程

当我们要写入一个值时,过程如下,以 jionghui 为例

  • 首先将 jionghui3Hash 函数分别计算,得到 bitSet 的下标为:1、7、10
  • bitSet 的这 3 个下标标记为 1

假设我们还有另外两个值:javadiaosi,按上面的流程跟 3Hash 函数分别计算,结果如下

  • javaHash 函数计算 bitSet 下标为:1、7、11
  • diaosiHash 函数计算 bitSet 下标为:4、10、11

在这里插入图片描述

查询流程

当我们要查询一个值时,过程如下,同样以 jionghui 为例

  • 首先将 jionghui3Hash 函数分别计算,得到 bitSet 的下标为:1、7、10
  • 查看 bitSet 的这 3 个下标是否都为 1,如果这 3 个下标不都为 1,则说明该值必然不存在,如果这 3 个下标都为 1,则只能说明可能存在,并不能说明一定存在

其实上图的例子已经说明了这个问题了,当我们只有值 jionghuidiaosi 时,bitSet 下标为 1 的有:1、4、7、10、11。当我们又加入值 java 时,bitSet 下标为 1 的还是这 5个,所以当 bitSet 下标为 1 的为:1、4、7、10、11 时,我们无法判断值 java 存不存在

其根本原因是,不同的值在跟 Hash 函数计算后,可能会得到相同的下标,所以某个值的标记位,可能会被其他值给标上了。这也是为啥布隆过滤器只能判断某个值可能存在,无法判断必然存在的原因。但是反过来,如果该值根据 Hash 函数计算的标记位没有全部都为 1,那么则说明必然不存在,这个是肯定的

缓存雪崩

缓存雪崩:当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了,会有大量的请求进来直接打到 DB 上,这样可能导致整个系统的崩溃,称为雪崩

在这里插入图片描述

解决方案

  • 过期时间打散:既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效
  • 热点数据不过期:该方式和缓存击穿一样,也是要着重考虑刷新的时间间隔和数据异常如何处理的情况

参考:https://blog.csdn.net/v123411739/article/details/115058811

猜你喜欢

转载自blog.csdn.net/weixin_38192427/article/details/115332207