Redis缓存穿透、缓存雪崩和缓存击穿

前言

Redis 缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带了一些问题。其中,最要害的是问题:数据一致性的问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么不能使用缓存。

另外一些典型的问题就是,缓存穿透、缓存雪崩、缓存击穿

一、缓存穿透

1、概念

查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。

2、造成原因

基本原因有:
(1)自身业务代码或者数据出现问题
(2)一些恶意攻击、 爬虫等造成大量空命中

3、解决方法

3.1、缓存空对象

(1)从缓存中去取,如果有就返回,如果没有就去数据库取。
(2)无论数据库有没有,都往缓存中设置一个对象,如果数据库没有就设置一个空对象,并设置过期时间。
(3)这样,当第二次再去缓存中取的时候,就会取到值,即使是一个空对象,但是这样解决了爬虫大量攻击数据库。

String get(String key) {
    // 从缓存中获取数据
    String cacheValue = cache.get(key);
    // 缓存为空
    if (StringUtils.isBlank(cacheValue)) {
        // 从存储中获取
        String storageValue = storage.get(key);
        cache.set(key, storageValue);
        // 如果存储数据为空, 需要设置一个过期时间(300秒)
        if (storageValue == null) {
            cache.expire(key, 60 * 5);
        }
        return storageValue;
    } else {
        // 缓存非空
        return cacheValue;
    }
}

3.2、布隆过滤器

用布隆过滤器先做一次过滤,对于不存在的数据布隆过滤器一般都能够过滤掉,不让请求再往后端发送。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在

布隆过滤器就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。
(1)add操作:向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash 算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。
(2)get操作:向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个key 不存在。如果都是 1,这并不能说明这个 key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致。如果这个位数组长度比较大,存在概率就会很大,如果这个位数组长度比较小,存在概率就会降低。
:布隆过滤器不能删除数据,如果要删除得重新初始化数据。
这种方法适用于数据命中不高、 数据相对固定、 实时性低(通常是数据集较大) 的应用场景, 代码维护较为复杂, 但是缓存空间占用很少。
在这里插入图片描述
布隆过滤器的使用
①引入相关的依赖

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.6.5</version>
</dependency>

②Java实现

public class RedissonBloomFilter {

    public static void main(String[] args) {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        //构造Redisson
        RedissonClient redisson = Redisson.create(config);

        RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
        //初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小
        bloomFilter.tryInit(100000000L,0.03);
        //将whr插入到布隆过滤器中
        bloomFilter.add("whr");

        //判断下面号码是否在布隆过滤器中
        System.out.println(bloomFilter.contains("naimei"));//false
        System.out.println(bloomFilter.contains("lulu"));//false
        System.out.println(bloomFilter.contains("whr"));//true
    }
}

3.3、设置空对象和布隆对象器一起使用

伪代码如下

//初始化布隆过滤器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆过滤器:预计元素为100000000L,误差率为3%
bloomFilter.tryInit(100000000L,0.03);
        
//把所有数据存入布隆过滤器
void init(){
    for (String key: keys) {
        bloomFilter.put(key);
    }
}

String get(String key) {
    // 从布隆过滤器这一级缓存判断下key是否存在
    Boolean exist = bloomFilter.contains(key);
    if(!exist){
        return "";
    }
    // 从缓存中获取数据
    String cacheValue = cache.get(key);
    // 缓存为空
    if (StringUtils.isBlank(cacheValue)) {
        // 从存储中获取
        String storageValue = storage.get(key);
        cache.set(key, storageValue);
        // 如果存储数据为空, 需要设置一个过期时间(300秒)
        if (storageValue == null) {
            cache.expire(key, 60 * 5);
        }
        return storageValue;
    } else {
        // 缓存非空
        return cacheValue;
    }
}

二、缓存击穿(缓存失效)

1、概念

缓存击穿是指一个 key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个 key 在失效的瞬间(过期),持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

与缓存穿透的区别:缓存穿透说的对象是根本不存在的值,而缓存击穿说的对象是一个热key过期所出现的问题

2、解决方法

2.1、预先设置热门数据

在redis高峰访问前,把一些热门数据提前存入redis中,加大这些热门数据key的时长实时调整 ,现场监控哪些数据是热门数据,实时调整key的过期时长(看情况设置)

2.2、加分布式锁

在缓存失效的时候(判断拿出来的值为空),不是立即去查数据库,先使用缓存工具的某些带成功操作返回值的操作。比如redis的setnx去set一个mutex key,当操作返回成功时(分布式锁),再查数据库,并回设重建缓存,最后删除mutex key。
当操作返回失败,证明有线程在查询数据库,当前线程睡眠一段时间在重试整个get缓存的方法。
重建缓存的伪代码如下:

String get(String key) {
    // 从Redis中获取数据
    String value = redis.get(key);
    // 如果value为空, 则开始重构缓存
    if (value == null) {
        // 只允许一个线程重建缓存, 使用nx, 并设置过期时间ex
        String mutexKey = "mutext:key:" + key;
        if (redis.set(mutexKey, "1", "ex 180", "nx")) {
             // 从数据源获取数据
            value = db.get(key);
            // 回写Redis, 并设置过期时间
            redis.setex(key, timeout, value);
            // 删除key_mutex
            redis.delete(mutexKey);
        }// 其他线程休息50毫秒后重试
        else {
            Thread.sleep(50);
            get(key);
        }
    }
    return value;
}

三、缓存雪崩

1、概念

缓存雪崩是指在某一个时间段,缓存集中过期失效。
由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降), 于是大量请求都会打到存储层, 存储层的调用量会暴增, 造成存储层也会级联宕机的情况。

2、造成原因

基本原因有两个:
(1)redis服务器挂掉了
(2)对缓存数据设置了相同的过期时间,导致某时间段内缓存集中失效

3、解决方法

(1) 保证缓存层(redis)服务高可用性:比如使用Redis Sentinel或Redis Cluster。
(2) 依赖隔离组件为后端限流熔断并降级
①使用Sentinel或Hystrix限流降级组件;
②服务降级:我们可以针对不同的数据采取不同的处理方式。当业务应用访问的是非核心数据(例如电商商品属性,用户信息等)时,暂时停止从缓存中查询这些数据,而是直接返回预定义的默认降级信息、空值或是错误提示信息;当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。
(3) 数据预热:在正式部署之前,把可能的数据线预先访问一遍,这样部分可能大量访问的数据就会加载到缓存。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
(4)设置不同的过期时间:设置缓存过期时间时加上一个随机值,避免缓存在同一时间过期。
设置不同的过期时间伪代码如下:

String get(String key) {
    // 从缓存中获取数据
    String cacheValue = cache.get(key);
    // 判断缓存是否为空
    if (StringUtils.isBlank(cacheValue)) {
        // 从db中获取
        String storageValue = storage.get(key);
        // 无论数据是否为空,将数据设置到缓存
        cache.set(key, storageValue);
        //设置一个过期时间(300到600之间的一个随机数),这样每个时间的过期时间就不一致了
        int expireTime = new Random().nextInt(300)  + 300;
        if (storageValue == null) {
            cache.expire(key, expireTime);
        }
        return storageValue;
    } else {
        // 缓存非空
        return cacheValue;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值