缓存穿透的原理及解决方案

缓存穿透

一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,应该去后端系统查询(比如数据库)。如果key对应的value不存在,并且同时对key并发请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透
在这里插入图片描述
比如我在数据库插入了10万的数据,ID自增,name是随机字符串。

@PostConstruct
    public void init(){
        long l = System.currentTimeMillis();
        allBy = testRepository.findAllBy();
        //创建布隆过滤器
        bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),allBy.size());
        for (TestEntity testEntity : allBy) {
            bloomFilter.put(testEntity.getName());
        }
        long l1 = System.currentTimeMillis();
        System.out.println(allBy.size()+"条数据耗时"+(l1 - l)/1000 +"s");
    }

代码启动时候把这十万条数据加入布隆过滤器。然后再生成一个随机字符串,模拟调用1000个线程用这个字符串同时去查询数据库和Redis
代码示例:

private static final int threadSize = 1000;

	@Test
    public void thread(){
        CyclicBarrier cyclicBarrier = new CyclicBarrier(threadSize);
        ExecutorService executorService = Executors.newFixedThreadPool(threadSize);
        for (int i=0; i<threadSize; i++){
            executorService.execute(new DemoApplicationTests().new MyThread(cyclicBarrier,redisTemplate,testRepository));
        }
    }
    
	public class MyThread implements Runnable {

        private CyclicBarrier cyclicBarrier;
        private RedisTemplate redisTemplate;
        private TestRepository testRepository;

        public MyThread(CyclicBarrier cyclicBarrier, RedisTemplate redisTemplate, TestRepository testRepository) {
            this.cyclicBarrier = cyclicBarrier;
            this.redisTemplate = redisTemplate;
            this.testRepository = testRepository;
        }

        @Override
        public void run() {
            try {
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            //随机生成字符串,布隆过滤器不存在
            String s = UUID.randomUUID().toString();
           
            ValueOperations<String,String> valueOperations = (ValueOperations<String,String>)redisTemplate.opsForValue();
            String s1 = valueOperations.get(s);
            if (null != s1){
                System.out.println("查询Redis");
                return;
            }
            synchronized (s){
                TestEntity byName = testRepository.findByName(s);
                if (null == byName){
                    System.out.println("Redis不存在,数据库不存在,发生缓存穿透");
                    return;
                }
            }
        }
    }    

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200419180629746.png?x-oss-proce在这里插入图片描述
之后就发生了数据库连接异常,如果在生产环境下,有人恶意攻击我们的系统,通过一个不存在的key大量并发去请求查询,就会造成系统的崩溃。

解决方法

最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

布隆过滤器的本质是一个合适大小的位数组加上一系列的哈希函数组成的
在这里插入图片描述
比如我们有一个数组,布隆过滤器会通过哈希算法对数组的每一个值进行哈希散列,然后分布到bit数组的某个位置,这个位置存储的不是数值本身,而是这个数值是否存在的一个标记,比如用1表示存在,0表示不存在。一般会有多次哈希求值,布隆的bit数组会根据原有数组的大小和需要过滤的精确度来确定大小和哈希散列的次数。

布隆过滤器用法:

一般用的比较多的是谷歌的BloomFilter,它是guava里面的一个工具类,可以导入坐标来加入依赖

		<dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>25.1-jre</version>
        </dependency>

主要方法

//创建布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), size);

BloomFilter.create方法可以创建一个布隆过滤器,第一个参数是设置字符集编码,第二个是原数组的大小,第三个是精确度(大于0小于1),可以不写,默认是0.03

我这里设置的数组大小是一百万,精确度默认,然后它创建的bit数组大小是7298440bit,转换MB就是0.87MB,需要的哈希散列次数是5次,小伙伴可以根据自己创建的数组大小和精确度自己设置。
原数组越大,创建的bit数组所需空间越大。
需要的精确度越高,哈希散列的次数越多。

在这里插入图片描述
加入布隆过滤器后再次重新生成字符串去查询,就会发现不存在的值已经被布隆过滤器过滤了。
在这里插入图片描述
当然最后也还会有一些漏网之鱼,这也是正常的,因为布隆过滤器不可能完全百分百的过滤掉不存在的值,它已经拦截掉了95%的非法查询。所以效率还是可以的。

但是如果我们需要去缓存里面删除某个值,那么谷歌的这个过滤器是没有删除方法的,可以用其他的布隆过滤器,有很多其他版本的布隆过滤器,实现了计数法,可以删除缓存里面的值

在0.0001的错误率下,插入量不到1.5亿的时候,bit已经到达了BitArray的最大容量了,这时如果再增加插入量,哈希函数个数就开始退化。到5亿的时候,哈希函数个数退化到了只有3个,也就是说,对每一个key,只有3位来标识,这时准确率就会大大下降。
这时有两种解决方案:
第一种当然就是减少预期插入量,1亿以内,还是可以保证理论上的准确率的。
第二种,如果你的系统很大,就是会有上亿的key,这时可以考虑拆分,将一个大的bloom filter拆分成几十个小的(比如32或64个),每个最多可以容纳1亿,这时整体就能容纳32或64亿的key了。查询的时候,先对key计算一次哈希,然后取模,查找特定的bloom filter即可

其他方法

缓存穿透另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
但是这种方法会占用一些系统资源,如果空值很多,可能会有些资源浪费

发布了6 篇原创文章 · 获赞 2 · 访问量 285

猜你喜欢

转载自blog.csdn.net/qq_42145271/article/details/105619173