八、缓存穿透

一、简介

请求去查询一条记录,先 redis 后 mysql 发现都查询不到该条记录,但是请求每次都会打到数据库上面去,导致后台数据库压力暴增,这种现象我们称为缓存穿透,这个redis变成了一个摆设。

 

危害:

第一次来查询后,一般我们有回写 redis 机制。第二次来查的时候 redis 就有了,偶尔出现穿透现象一般情况无关紧要。

要防止恶意攻击,用 redis 和 mysql 都不存在的数据,不断查询。

 

二、解决方案1:空对象缓存或者缺省值

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

id相同打你系统。第一次打到mysql,空对象缓存后第二次就返回null了,避免mysql被攻击,不用再到数据库中去走一圈了。

id不同打你系统。由于存在空对象缓存和缓存回写(看自己业务不限死),redis中的无关紧要的key也会越写越多(记得设置redis过期时间)。

 

 

三、解决方案2:Google布隆过滤器Guava解决缓存穿透

Guava适合应用在单机项目。

1、pom文件

        <!--guava Google 开源的 Guava 中自带的布隆过滤器-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>23.0</version>
        </dependency>

2、HelloWorld级的demo

    @Test
    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));
    }

3、100w数据的误判率的demo

public class BloomfilterDemo {
    public static final int _1W = 10000;
    //布隆过滤器里预计要插入多少数据
    public static int size = 100 * _1W;
    //误判率
    public static double fpp = 0.03;

    public static void main(String[] args) {
        // 构建布隆过滤器
        BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);
        //  1 先往布隆过滤器里面插入100万的样本数据
        for (int i = 0; i < size; i++) {
            bloomFilter.put(i);
        }
        //  故意取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());
    }
}

输出:

...
1099707 被误判了.
1099850 被误判了.
1099882 被误判了.
1099947 被误判了.
误判的数量:3033

 4、误判率分析

 fpp=0.03,它越小误判的个输也就越少。这样是不是可以设置无限小?

源码底层会根据设置的误判率,不断添加hash函数和布隆过滤器的 bit 位。因此误判率设置的越小,内存开销越大,执行效率也越低,计算量急剧增大。0.03 是默认系数。

5、布隆过滤器常用方案

 

 

四、解决方案3:Redis布隆过滤器解决缓存穿透

为了Guava只能单机使用的这个问题,诞生了 Redis 中的布隆过滤器。

1、白名单demo

 

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;
    static RBloomFilter rBloomFilter = null;

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

        rBloomFilter.tryInit(size, fpp);

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

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

        //  测试三、都没有

    }

    public static void main(String[] args) {
        String phoneListById = getPhoneListById("10086");
        System.out.println("------查询出来的结果: " + phoneListById);

        //暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        redissonClient.shutdown();
    }

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

 

posted @ 2022-01-20 15:57  幻月hah  阅读(116)  评论(0编辑  收藏  举报