Redis 缓存穿透、击穿和雪崩

1. 缓存穿透

1.1 概念

请求的 key 在缓存和数据源中都不存在,就会导致每次请求都访问到数据源,失去了缓存的意义。

1.1.1 示例代码
@Override
public Goods searchArticleById(Long goodsId){
    Object object = redisTemplete.opsForValue().get(String.valueOf(goodsId));
    
    //缓存查询命中
    if(object != null){
    	return Goods(object);
    }
    
    //从数据源中查询
    Goods goods = goodsMapper.selectByPrimaryKey(goodsId);
    if(goods != null){
        //将查询结果放入缓存
        redisTemplete.opsForValue().set(String.valueOf(goodsId),goods,60,TimeUnit.MINUTES);
    }
    return goods;
}

1.2 解决方案

1.2.1 缓存空值

如果一个查询没有从数据源获取到数据,不管它是真的不存在还是存在故障,我们都把这个空结果进行缓存,但是要设置一个较短的过期时间。这样在短时间内,再次查询就不会继续去访问数据源了。

1.2.1.1 实例代码
@Override
public Goods searchArticleById(Long goodsId){
    Object object = redisTemplete.opsForValue().get(String.valueOf(goodsId));
    
    //缓存查询命中
    if(object != null){
    	return Goods(object);
    }
    
    //从数据源中查询
    Goods goods = goodsMapper.selectByPrimaryKey(goodsId);
    if(goods != null){
         //将查询结果放入缓存
        redisTemplete.opsForValue().set(String.valueOf(goodsId),goods,60,TimeUnit.MINUTES);
    }else{
        //缓存空值
        redisTemplete.opsForValue().set(String.valueOf(goodsId),goods,60,TimeUnit.SECONDS);
    }
        
    return goods;
}
1.2.2 布隆过滤器

将所有可能请求的数据放入一个足够大 bitmap 中,一个不存在的请求数据就会被拦截到,避免对数据源的冲击。

1.2.2.1 什么是布隆过滤器

布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间越来越大。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为:O (n), O (log n), O (n/k)。

布隆过滤器的原理是,当一个元素被加入集合时,通过 K 个 Hash 函数将这个元素映射成一个位数组中的 K 个点,把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了:如果这些点有任何一个 0,则被检元素一定不在;如果都是 1,则被检元素很可能在。这就是布隆过滤器的基本思想。

1.2.2.2 布隆过滤器的实现

在实际应用当中,我们不需要自己去实现 BloomFilter。可以使用 Guava 提供的相关类库即可。

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>25.1-jre</version>
</dependency>
public class Test1 {

    private static int size = 1000000;

    //定义错误率
    private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size,0.01);

    public static void main(String[] args) {
        for (int i = 0; i < size; i++) {
            bloomFilter.put(i);
        }

        long startTime = System.nanoTime(); // 获取开始时间
        //判断这一百万个数中是否包含29999这个数
        if (bloomFilter.mightContain(29999)) {
            System.out.println("命中了");
        }
        long endTime = System.nanoTime();   // 获取结束时间
        System.out.println("程序运行时间: " + (endTime - startTime) + "纳秒");
    }

}

布隆过滤器本身存在一定的不准确性。

1.2.3 接口限流与熔断、降级

在一定时间流量超出预期后,拒绝预期的访问,给予用户友好提示。

2. 缓存击穿

2.1 概念

某一个 key 访问流量非常大,大并发集中对这个 key 进行发问,当这个 key 失效的瞬间,持续的大并发直接落到了数据源上,对数据源造成冲击。

2.2 解决方案

2.2.1 热点键永不过期

如果可以话,热点键不设置过期时间,这样也就不存在击穿的问题。

2.2.2 互斥锁

当缓存失效的时候,使用互斥锁来让请求进行排队。这样并没有提供并发量,只是解决了可能会造成的数据源的压力。

2.2.2.1 示例代码
@Override
public Goods searchArticleById(Long goodsId){
    Object object = CacleUtils.get(String.valueOf(goodsId));
    
    //缓存查询命中
    if(object != null){
    	return Goods(object);
    }
    
    //先尝试获取一把分布式锁
    Goods goods = null;
    try{
        Boolean result = ReenLock.tryLock(key_mutex,requestId,60000);
        if(result){
            //从数据源中查询
            goods = goodsMapper.selectByPrimaryKey(goodsId);
            if(goods !=null){
                CacheUtils.set(String.valueOf(goodsId),goods,60000);
            }
        }else{
            //稍后再去尝试
            Thread.sleep(100);
            result = searchArticleById(goodsId);
        }
    }finally{
        ReenLock.unLock(key_mutex,requestId);
    }
    
    return goods;
}

3. 缓存雪崩

3.1 概念

缓存雪崩,是指在某一个时间段,缓存集中过期失效。

3.2 解决方案

3.2.1 加锁排队

使用互斥锁,让来进来的请求进行排队处理。

3.2.2 不同过期时间

既然缓存雪崩时同一时间点,大量的key过期,导致请求落到数据源上,那就尽量让不同的键设置不同的过期时间,尽量让缓存失效的时间分布均匀。

3.2.3 双层缓存策略

C1 为原始缓存,C2 为拷贝缓存,C1 失效时,可以访问 C2,C1 缓存失效时间设置为短期,C2 设置为长期。

3.2.4 数据预热

解决冷启动造成的雪崩,可以提前预估热点数据,将相关的缓存数据直接加载到缓存系统,避免冷启动。

3.2.5 热点永不过期

不过期自然也就没有这个问题。

上一篇:java比较器


下一篇:Java 比较器