微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

从零开始SpringCloud Alibaba实战86——再谈缓存击穿、缓存穿透、缓存雪崩

文章目录

缓存击穿

什么时候Redis中没有要查询的数据呢?答案是过期和新增:过期:在Redis会有一个key值,每个key值都有一个ttl,也就是生命周期,一旦过期了就只能存在数据库里了;新增:插入,更新的数据还未来得及同步到Redis中。
如果线程查询一个数据库中不存在的值,此时从数据库中就返回一个空值。当一个线程不断执行同一条查询语句查询这个Redis数据库都不存在的数据时,比如说执行上万次同一条查询语句,那么每次都穿过Redis,这样Redis就没有意义了。那么为了解决这个问题,就可以在Redis中设置空值,同一条查询语句对应的key(数据库中没有的值)都对应着一个空值,当查询其他查询语句也查询数据库中不存在的值时,都要对每个key设置一个空值,随着越来越多这样的key,Redis服务器就会承受不了这么大的压力,例如下面的例子就引出了Redis缓存穿透:

缓存击穿: 一个并发访问量比较大的key在某个时间过期,导致所有的请求直接打在DB上。

在这里插入图片描述

缓存击穿如何解决

加锁更新

查询缓存,发现缓存中不存在,加锁,让其它线程等待,只让一个线程去更新缓存。
方法是比较普遍的做法,即,在根据key获得的value值为空时,先锁上,再从数据库加载,加载完毕,释放锁。若其他线程发现获取锁失败,则睡眠50ms后重试。

至于锁的类型,单机环境用并发包的Lock类型就行,集群环境则使用分布式锁( redis的setnx)
单机伪代码

static Lock reenLock = new reentrantlock();

//最后使用互斥锁的方式来实现.
  public List<String> getData() throws InterruptedException {
        List<String> result = new ArrayList<String>();
        // 从缓存读取数据
        result = getDataFromCache();
        if (result.isEmpty()) {
            if (reenLock.tryLock()) {
                try {
                    System.out.println("我拿到锁,从DB获取数据库后写入缓存");
                    // 从数据库查询数据
                    result = getDataFromDB();
                    // 将查询到的数据写入缓存
                    setDataToCache(result);
                } finally {
                    reenLock.unlock();// 释放锁
                }
            } else {//我没有拿到锁
                result = getDataFromCache();// 先查一下缓存
                if (result.isEmpty()) {
                    System.out.println("我没拿到锁,缓存也没数据,等一下");
                    Thread.sleep(100);// 
                    return getData();// 递归调用重试
                }
            }
        }
        return result;
        }

集群环境的redis代码如下所示:

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);  

    }  

  }  

}  

在这里插入图片描述

优点

思路简单

保证一致性

缺点

代码复杂度增大

存在死锁的风险

异步构建缓存

在这种方案下,构建缓存采取异步策略,会从线程池中取线程来异步构建缓存,从而不会让所有的请求直接怼到数据库上。该方案redis自己维护一个timeout,当timeout小于System.currentTimeMillis()时,则进行缓存更新,否则直接返回value值。

集群环境的redis代码如下所示:

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;  

    }

优点

性价最佳,用户无需等待

缺点

无法保证缓存一致性

布隆过滤器

布隆过滤器的巨大用处就是,能够迅速判断一个元素是否在一个集合中。因此他有如下三个使用场景:

网页爬虫对URL的去重,避免爬取相同的URL地址

垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱(同理,垃圾短信)

缓存击穿,将已存在的缓存放到布隆过滤器中,当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉。

OK,接下来我们来谈谈布隆过滤器的原理

其内部维护一个全为0的bit数组,需要说明的是,布隆过滤器有一个误判率的概念,误判率越低,则数组越长,所占空间越大。误判率越高则数组越小,所占的空间越小。

private static BloomFilter bloomFilter =BloomFilter.create(Funnels.integerFunnel(), size);
String get(String key) {

String value = redis.get(key);

if (value == null) {

    if(!bloomfilter.mightContain(key)){
        return null;

    }else{
       value = db.get(key);  

       redis.set(key, value);  

    }

}

return value;

}

缓存穿透

什么是缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

在这里插入图片描述

与上面讲到的缓存击穿的区别:
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

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

自身业务代码问题
恶意攻击,爬虫造成空命中

解决方

缓存空值/认值

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

缓存空值有两大问题:

空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的

方法是针对这类数据设置一个较短的过期时间,让其自动剔除。

缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。

例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致。

这时候可以利用消息队列或者其它异步方式清理缓存中的空对象。

布隆过滤器

除了缓存空对象,我们还可以在存储和缓存之前,加一个布隆过滤器,做一层过滤。

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

缓存雪崩

简单说:由于缓存不可用,导致大量请求访问后端服务,可能 MysqL 扛不住高并发而打死, 像滚雪球一样,影响越来越大,最后导致整个网站崩溃不可用

至于为什么会像滚雪球一样?整个与整个系统的架构有关;

redis 集群彻底崩溃:不可用

缓存服务在请求 redis 时,会有大量的线程阻塞,占用资源

超时请求失败之后,会去 MysqL 查询原始数据,MysqL 抗不住,被打死

源头服务由于 MysqL 被打死,对源服务的请求也被阻塞,占用资源

缓存服务大量的资源全部耗费在访问 redis 和 源服务上;最后自己被拖死,无法提供服务

Nginx 无法访问缓存服务,只能基于本地缓存提供服务,当缓存过期后,就耗费在访问缓存服务上

最后整个网站崩溃,页面加载不出来任何数据

考虑的比较完善的一套方案,分为事前、事中、事后三个层次去思考再怎么来应对缓存雪崩的场景

事前解决方

发生缓存雪崩之前,事情之前,怎么去避免 redis 彻底挂掉

redis本身的高可用性、复制、主从架构,操作主节点,读写,数据同步到从节点,一旦主节点挂掉,从节点跟上

双机房部署,一套 redis cluster,部分机器在一个机房,另一部分机器在另外一个机房

还有一种部署方式,两套 redis cluster,两套 redis cluster 之间做一个数据的同步,redis 集群是可以搭建成树状的结构的

一旦说单个机房出了故障,至少说另外一个机房还能有些 redis 实例提供服务

事中解决方

redis cluster 已经彻底崩溃了,已经开始大量的访问无法访问到 redis

ehcache 本地缓存
所做的多级缓存架构的作用上了 ,ehcache 的缓存应对零散的 redis 中数据被清除掉的现象,另外一个主要是预防 redis 彻底崩溃

多台机器上部署的缓存服务实例的内存中,还有一套 ehcache 的缓存,还能支撑一阵

对 redis 访问的资源隔离
对 redis 访问使用 hystrix 进行隔离,防止自己资源大量阻塞在访问 redis

对源服务访问的限流以及资源隔离
同上,防止自己资源大量阻塞在访问源服务上,同时 hystrix 在资源隔离时也做到了限流

事后解决方

redis 数据可以恢复,之前讲解过各种备份机制,redis 数据备份和恢复,redis 重新启动起来

redis 数据彻底丢失了或者数据过旧,快速缓存预热,redis 重新启动起来

由于事中做了限流与隔离,缓存服务不会被打死,通过熔断策略 和 half-open 策略, 可以自动可以恢复对 redis 的访问,发现 redis 可以访问了,就自动恢复了

熔断降级

服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,暂时停止业务服务访问缓存系统。
服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的 fallback(退路)错误处理信息。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐