redis 缓存穿透、雪崩、击穿

一、缓存穿透 

概念

缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。

“穿透”的意思就是穿过redis进入到DB。

下图是一个简单的缓存实现:

解决方案

一般情况下,先查询缓存是否有该条数据,缓存中没有时,再查询数据库。当数据库也不存在该条数据时,每次查询都要访问数据库,这就是缓存穿透。缓存穿透带来的问题是,当有大量请求查询数据库不存在的数据时,就会给数据库带来压力,甚至会拖垮数据库。

可以使用布隆过滤器解决缓存穿透的问题,把已存在数据的key存在布隆过滤器中。当有新的请求时,先到布隆过滤器中查询是否存在,如果不存在该条数据直接返回;如果存在该条数据再查询缓存查询数据库。

 

1. 布隆过滤器

布隆过滤器(Bloom Filter),是1970年,由一个叫布隆的小伙子提出的。

它其实是一个很长的二进制向量和一系列随机映射函数,二进制应该都清楚,存储的数据不是0就是1,默认是0。主要用于判断一个元素是否在一个集合中,0表明不存在某个数据,1表明存在某个数据。

布隆过滤器用途

  • 解决Redis缓存穿透缓存

  • 在爬虫时,对爬虫网址进行过滤,已经存在布隆中的网址,不在爬取

  • 垃圾邮件过滤,对每个发送邮件的地址进行判断是否在布隆的黑名单中,若是在则判断为垃圾邮件

 

布隆过滤器原理

Redis中布隆过滤器的数据结构就是一个很大的位数组和几个不一样的无偏哈希函数(能把元素的哈希值算得比较平均,能让元素被哈希到位数组中的位置比较随机)。如下图,A、B、C就是三个这样的哈希函数,分别对“OneMoreStudy”和“万猫学社”这两个元素进行哈希,位数组的对应位置则被设置为1:

 

向布隆过滤器中添加元素时,会使用多个无偏哈希函数对元素进行哈希,算出一个整数索引值,然后对位数组长度进行取模运算得到一个位置,每个无偏哈希函数都会得到一个不同的位置。再把位数组的这几个位置都设置为1,这就完成了bf.add命令的操作。

向布隆过滤器查询元素是否存在时,和添加元素一样,也会把哈希的几个位置算出来,然后看看位数组中对应的几个位置是否都为1,只要有一个位为0,那么就说明布隆过滤器里不存在这个元素。如果这几个位置都为1,并不能完全说明这个元素就一定存在其中,有可能这些位置为1是因为其他元素的存在,这就是布隆过滤器会出现误判的原因。

误判现象

假设某个元素通过映射对应下标为4,5,6这3个点。虽然这3个点都为1,但是很明显这3个点是不同元素经过哈希得到的位置,因此这种情况说明元素虽然不存在于集合中,也可能对应的都是1,这是存在误判的原因。比如只存了"你好",那么用"hello"来查询的时候,会判断"hello"也存在集合中(假设它们计算出的一组hash值都是一样的)。

 

删除困难

这个操作在布隆过滤器中是不允许的,根据其原理我们就知道,如果将是1的位置重置成0可能会影响到其他元素是不是在集合中的判断。

布隆过滤器的初衷是用少许的误判来实现省空间+高性能,所以我们允许这样的误判。

 

具体使用

在信息写入的时候,会同时写入到缓存(部分数据)、数据库和布隆过滤器(所有数据)。需要查询的时候,先进行对缓存进行查询,如果找不到的话,就会先使用 BloomFilter 进行判断是否存在,如果存在就会继续向数据库查询;如果不存在,就直接返回空了。

这样在很大程度上提升了应用服务的效率!

假设,此时黑客一秒发送5000 个请求,来查询用户详情,且这些请求中大量的userid的数据缓存和布隆过滤器中都不存在,那么可以直接过滤掉这样的请求,避免频繁查数据库。其实很好理解,布隆过滤器,相当于缓存和数据库之间的屏障,用于拦截无效请求,极大地减少了穿透的数量。

2. 缓存空值

如果第一次请求查询返回的数据为空,把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。这样,第二次请求缓存就能命中,即在缓存中获取空数据,而不会继续访问数据库。我的理解是,缓存是第一层,布隆过滤器是第二层,关系型数据库是第三层,该方法做到了重复的请求在第一层被拦截,之后进入二层拦截处理。

3.接口限流与熔断、降级

重要的接口一定要做好限流策略,防止用户恶意或频繁刷接口,同时要做好降级处理,当接口中的某些服务不可用的时候,进行熔断、失败快速返回。

 

二、缓存雪崩

概念

当缓存服务器重启或者大量缓存集中在某一个时间段失效,此时,会给后端系统(比如DB)带来很大压力。

举个例子:如果一个首页的数据,进行了缓存,并且所有Key的失效时间都是12小时。中午12点进行刷新,然后晚上零点有个大促活动,此时大量用户涌入,假设每秒6000个请求,原本缓存可以抗住每秒5000个请求(即存在相应的缓存数据,可以缓存命中),但是缓存中所有Key如果此时都失效了,那么6000个/秒的请求全部打在了数据库上,数据库必然扛不住。

 

解决方案

1.加锁排队(使用互斥锁排队,缓存穿透中亦可使用)

业界比价普遍的一种做法,即根据key获取value值为空时,即缓存中查数据没有查到,就加锁,再去数据库中查询数据,此时大量的无效请求被阻塞(控制了穿透量),最后释放锁。注意,分布式环境中要使用分布式锁(Redis 的话使用SETNX 方法),单机的话普通的锁(synchronized、Lock)就行了。

另外,说一说java多线程、并发请求、tomcat多线程

Tomcat使用多线程来处理HTTP请求,可以使用BIO(阻塞的)模型或者NIO模型(非阻塞的)。NIO是非阻塞IO,基于IO多路复用技术(例如Reactor模式)实现,只需要一个线程或者少量线程,就可以处理大量请求。Java多线程是用来处理业务逻辑的。

2. 数据预热

场景:redis中很多数据没有缓存,此时如果大量请求涌入,会去查询DB,这会压垮DB,导致其无法正常使用。

缓存预热就是系统上线后,将可能涉及的缓存数据(不可能是所有的数据,一般是热点数据)直接加载到缓存系统。这样就可以避免在用户请求的时候,需要再去查询数据库,然后再将数据缓存的问题。如果用户直接查询事先被预热的缓存数据,可以大大减轻DB的压力。数据预热能够提高读取速度。

3.双缓存方案

C1为原始缓存(主缓存),C2为备份缓存。C1失效时,可以访问C2,C1缓存失效时间设置为短期,C2设置为长期。主缓存更新时需要同步更新备份缓存。(需要考虑脏数据问题)

4. 热点数据永不过期

用户访问特别频繁的热点数据,设置永不过期。这也意味着Redis 需要更多的存储空间。

5.使用限流降级组件

使用常见的限流降级组件如 Hystrix、Sentinel 等。

6. 优化缓存数据的过期时间

设计缓存时,为每一个 key 选择合适的过期时间,避免大量的 key 在同一时刻同时失效,造成缓存雪崩。(让过期时间尽可能的分散且均匀分布)

7.定时续命

通过定时任务,定时的去刷新缓存的失效时间(续命),比如某数据的缓存失效时间设置了一小时,在失效之前,重置该数据的缓存失效时间为一小时。

 

三、缓存击穿

概念

缓存击穿是指一个 Key 非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,假设这个 Key瞬间失效,持续的大量请求打到了数据库上,使数据库压力过大,甚至使数据库服务挂掉。比如双十一的秒杀商品。

解决方案

  1. 热点key永不过期

  2. 单机使用互斥锁,首先大量的用户请求先去redis查询数据,如果有的话就返回给用户,如果没有的话就查询数据库,在查询数据库之前进行加锁,那么此时就只有一个线程可以拿到锁,对数据库的压力就很小,查询到数据之后,把数据缓存起来,其它没有抢到锁的线程让它们睡眠一会,然后去redis查询刚才已缓存的数据;分布式使用分布式锁。

public static String getData(String key) throws InterruptedException {
        //从Redis查询数据
        String result = getDataByKV(key);
        //参数校验
        if (StringUtils.isBlank(result)) {
            try {
                //获得锁
                if (reenLock.tryLock()) {
                    //去数据库查询
                    result = getDataByDB(key);
                    //校验
                    if (StringUtils.isNotBlank(result)) {
                        //插进缓存
                        setDataToKV(key, result);
                    }
                } else {
                    //睡一会再拿,保证该线程拿到刚才缓存的数据并返回
                    Thread.sleep(100L);
                    result = getData(key);
                }
            } finally {
                //释放锁
                reenLock.unlock();
            }
        }
        return result;
}

  

四、缓存穿透和缓存击穿的区别

我是这样理解的。缓存穿透,相当于大量请求穿过redis缓存,打到数据库上,此时这些请求就像一堆子弹,redis就像一道屏障,这些子弹任意穿过屏障,留下许多小孔。缓存击穿,相当于针对某热点数据的大量请求穿过redis缓存,打到数据库上,此时这些请求就像一堆子弹,redis就像一道屏障,子弹集中一点穿过屏障,留下一个大孔。简单的说,缓存穿透是针对多个key的,缓存击穿是针对单个热点key的。

五、最后小结

  1. 缓存雪崩:大量的缓存key在同一时间失效。

  2. 缓存穿透:大量的请求在redis中没有相应的缓存数据,穿过redis,进入数据库。

  3. 缓存击穿:针对某一个热点key进行大量请求,在该key突然失效时,这些请求都进入数据库。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值