Redis6系列9-缓存雪崩、缓存击穿、缓存穿透

557 阅读6分钟

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

Redis作为目前使用最广泛的缓存,相信大家都不陌生。但是使用缓存并没有这么简单,还要考虑缓存雪崩,缓存击穿,缓存穿透的问题,什么是缓存雪崩,击穿,穿透呢,出现这些问题又怎么解决呢?

1. 缓存雪崩

当某一个时刻出现大规模的缓存失效的情况,那么就会导致大量的请求直接打在数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机。这时候如果运维马上又重启数据库,马上又会有新的流量把数据库打死。这就是缓存雪崩。 image.png

1.1 分析原因

造成缓存雪崩的关键在于在同一时间大规模的key失效。为什么会出现这个问题呢,有几种可能:

  • redis主机挂了,Redis群盘崩溃
  • 缓存中大量数据同时过期

1.2 解决办法

image.png

  • redis集群实现高可用(主从+哨兵,Redis Cluster)

  • encache缓存+Hystrix或者阿里sentine限流&降级:当流量到达一定的阈值时,就直接返回“系统拥挤”之类的提示,防止过多的请求打在数据库上。至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。

  • 开启Redis持久化机制aof/rdb,尽快恢复缓存集群

  • 提高数据库的容灾能力,可以使用分库分表,读写分离的策略

  • 数据预热:对于即将来临的大量请求,我们可以提前走一遍系统,将数据提前缓存在Redis中,并设置不同的过期时间(比如1-5分钟随机)。

2. 缓存击透

其实跟缓存雪崩有点类似,缓存雪崩是大规模的key失效,而缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增。这种现象就叫做缓存击穿。

2.1 分析原因

关键在于某个热点的key失效了,导致大并发集中打在数据库上。所以要从两个方面解决,第一是否可以考虑热点key不设置过期时间第二是否可以考虑降低打在数据库上的请求数量

危害:会造成某一时刻数据库请求量过大,压力暴增

2.2 解决办法

2.2.1 不设置过期时间

如果业务允许的话,对于热点的key可以设置永不过期的key。

2.2.2 互斥独占锁防止击穿

如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库打死。当然这样会导致系统的性能变差。

多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上 使用一个 互斥锁来锁住它。 其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。

image.png

2.2.3 异步更新

还有个可行的方案就是把缓存设置为永久不过期,异步定时更新缓存。比如后台有个值守线程专门定时更新缓存,但一般还要定时频繁地去检测缓存,一旦发现被踢掉(比如被缓存的失效策略 FIFO、LFU、LRU 等)需要立刻更新缓存,但这个“定时”的度是比较难掌握的,实现简单但用户体验一般。

异步更新机制还比较适合缓存预热,缓存预热是指系统上线后,将相关的缓存数据直接加载到缓存系统,避免在用户请求时才缓存数据,提高了性能。

3. 缓存穿透

我们使用Redis大部分情况都是通过Key查询对应的值,假如发送的请求传进来的key是不存在Redis中的,那么就查不到缓存,查不到缓存就会去数据库查询。假如有大量这样的请求,这些请求像“穿透”了缓存一样直接打在数据库上,这种现象就叫做缓存穿透。

3.1 分析原因

关键在于在Redis查不到key值,这和缓存击穿有根本的区别,区别在于缓存穿透的情况是传进来的key在Redis中是不存在的。假如有黑客传进大量的不存在的key,那么大量的请求打在数据库上是很致命的问题,所以在日常开发中要对参数做好校验,一些非法的参数,不可能存在的key就直接返回错误提示,要对调用方保持这种“不信任”的心态。

3.2 解决办法

3.2.1 空对象缓存或者缺省值

如果Redis查不到数据,数据库也查不到,我们把这个Key值保存进Redis,设置value="null",当下次再通过这个Key查询时就不需要再查询数据库。

这种处理方式肯定是有问题的,假如传进来的这个不存在的Key值每次都是随机的,那存进Redis也没有意义。例如:黑客或恶意攻击

黑客会对你的系统进行攻击,拿一个不存在的id去查询数据,会产生大量的请求到数据库查询,可能会导致你的数据库由于压力过大而宕机。

  • id相同攻击系统:第一次打到mysql,空对象缓存后第二次就会返回null,就不会访问数据库了
  • id不同攻击系统:由于存在空对象缓存和缓存回写,redis中无用的key会越来越多(设置redis的过期时间

3.2.2 Guava布隆过滤器解决缓存穿透

image.png

代码如下:

public class GuavaBloomfilterDemo {
    public static final int _1W = 10000;
    //布隆过滤器里预计要插入多少数据
    public static int size = 100 * _1W;
    //误判率,它越小误判的个数也就越少(思考,是不是可以设置的无限小,没有误判岂不更好)
    public static double fpp = 0.01;

    /**
     * helloworld入门
     */
    public void bloomFilter() {
        // 创建布隆过滤器对象
        BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), 100);
        // 判断指定元素是否存在
        System.out.println(filter.mightContain(1));
        System.out.println(filter.mightContain(2));
        // 将元素添加进布隆过滤器
        filter.put(1);
        filter.put(2);
        System.out.println(filter.mightContain(1));
        System.out.println(filter.mightContain(2));

    }

    /**
     * 误判率演示+源码分析
     */
    public void bloomFilter2() {
        // 构建布隆过滤器
        BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(),size,fpp);

        //1 先往布隆过滤器里面插入100万的样本数据
        for (int i = 0; i < size; i++) {
            bloomFilter.put(i);
        }
       /* List<Integer> listSample = new ArrayList<>(size);
        //2 这100万的样本数据,是否都在布隆过滤器里面存在?
        for (int i = 0; i < size; i++)
        {
            if (bloomFilter.mightContain(i)) {
                listSample.add(i);
                continue;
            }
        }
        System.out.println("存在的数量:" + listSample.size());*/

        //3 故意取10万个不在过滤器里的值,看看有多少个会被认为在过滤器里,误判率演示
        List<Integer> list = new ArrayList<>(10 * _1W);

        for (int i = size+1; i < size + 100000; i++) {
            if (bloomFilter.mightContain(i)) {
                System.out.println(i+"\t"+"被误判了.");
                list.add(i);
            }
        }
        System.out.println("误判的数量:" + list.size());
    }

    public static void main(String[] args)
    {
        new GuavaBloomfilterDemo().bloomFilter2();
    }
}

Guava 提供的布隆过滤器的实现还是很不错的 (想要详细了解的可以看一下它的源码实现),但是它有一个重大的缺 陷就是只能单机使用  ,而现在互联网一般都是分布式的场景。为了解决这个问题,我们就需要用到 Redis 中的布隆过滤器了

3.2.3 redis布隆过滤器

案例1:白名单过滤器

  1. 架构说明:

image.png

  1. 误判问题,但是概率小可以接受,不能从布隆过滤器删除元素
  2. 全部合法的key都需要放入过滤器+redis里面,不然数据就会返回null

代码如下:

public class RedissonBloomFilterDemo {
    public static final int _1W = 10000;

    //布隆过滤器里预计要插入多少数据
    public static int size = 100 * _1W;
    //误判率,它越小误判的个数也就越少
    public static double fpp = 0.03;

    static RedissonClient redissonClient = null;//jedis
    static RBloomFilter rBloomFilter = null;//redis版内置的布隆过滤器

    static {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.111.147:6379").setDatabase(0);
        //构造redisson
        redissonClient = Redisson.create(config);
        //通过redisson构造rBloomFilter
        rBloomFilter = redissonClient.getBloomFilter("phoneListBloomFilter",new StringCodec());

        rBloomFilter.tryInit(size,fpp);

        // 1测试  布隆过滤器有+redis有
        //rBloomFilter.add("10086");
        //redissonClient.getBucket("10086",new StringCodec()).set("chinamobile10086");

        // 2测试  布隆过滤器有+redis无
        //rBloomFilter.add("10087");

        //3 测试 ,布隆过滤器无+redis无

    }

    private static String getPhoneListById(String IDNumber) {
        String result = null;

        if (IDNumber == null) {
            return null;
        }
        //1 先去布隆过滤器里面查询
        if (rBloomFilter.contains(IDNumber)) {
            //2 布隆过滤器里有,再去redis里面查询
            RBucket<String> rBucket = redissonClient.getBucket(IDNumber, new StringCodec());
            result = rBucket.get();
            if(result != null) {
                return "i come from redis: "+result;
            }else{
                result = getPhoneListByMySQL(IDNumber);
                if (result == null) {
                    return null;
                }
                // 重新将数据更新回redis
                redissonClient.getBucket(IDNumber, new StringCodec()).set(result);
            }
            return "i come from mysql: "+result;
        }
        return result;
    }

    private static String getPhoneListByMySQL(String IDNumber) {
        return "chinamobile"+IDNumber;
    }



    public static void main(String[] args) {
        //String phoneListById = getPhoneListById("10086");
        //String phoneListById = getPhoneListById("10087"); //请测试执行2次
        String phoneListById = getPhoneListById("10088");
        System.out.println("------查询出来的结果: "+phoneListById);

        //暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(1);

        }catch (InterruptedException e) {
            e.printStackTrace();
        }
        redissonClient.shutdown();
    }
}

案例2:黑名单过滤器

image.png

大家自行编写代码。

4. 总结

image.png

参考文档: 缓存穿透、缓存击穿、缓存雪崩,看这篇就够了
什么是缓存雪崩、缓存击穿、缓存穿透
缓存雪崩、缓存击穿、缓存穿透