浅谈恶意请求造成缓存穿透和布隆过滤器

一、问题场景:

       系统后台对于一些热点数据的访问,为保证API的相应速度,普遍的做法就是将数据预热到内存中减少对数据库的访问,而考虑到key的过期问题或者懒加载问题,兜底的处理就是当缓存没有命中时,触发数据库查询并将数据同步至缓存中。
       如果请求没有命中缓存,并且请求的数据在数据库中根本不存在,在缓存中就无法保存此类key的value值(即使保存此key把value置为空,但key的边界无限大,也就意味着需要的服务器内存也就无限大,显然不现实),客户端同时间发送大量请求可能会导致数据库负载过大宕掉。

二、解决思路:

       最有效的方法就是限制数据边界,对于一些数据库不存在的数据,要把请求挡在repo层之前。如果表中数据量过大,全部进行缓存到redis或者hashmap中也不现实,此时就可以考虑布隆过滤器。

三、布隆过滤器介绍

       Bloom Filter是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。Bloom Filter的这种高效是有一定代价的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。
优点:空间效率和查询时间都远远超过一般的算法。
缺点:有一定的误识别率,删除困难。
下面我们具体来看Bloom Filter是如何用位数组表示集合的。初始状态时,Bloom Filter是一个包含m位的位数组,每一位都置为0。

在这里插入图片描述

为了表达S={x, x,…,x}这样一个n个元素的集合,Bloom Filter使用k个相互独立的哈希函数(Hash Function),它们分别将集合中的每个元素映射到{1,…,m}的范围中。对任意一个元素x,第i个哈希函数映射的位置h(x)就会被置为1(1≤i≤k)。注意,如果一个位置多次被置为1,那么只有第一次会起作用,后面几次将没有任何效果。在下图中,k=3,且有两个哈希函数选中同一个位置(从左边数第五位)。

在这里插入图片描述

在判断y是否属于这个集合时,我们对y应用k次哈希函数,如果所有h(y)的位置都是1(1≤i≤k),那么我们就认为y是集合中的元素,否则就认为y不是集合中的元素。下图中y就不是集合中的元素。y或者属于这个集合,或者刚好是一个false positive。
在这里插入图片描述

四、布隆过滤器使用

1、根据原理手动实现

测试demo如下:

public class MyBloomFilter {
 
    /**
     * 一个长度为10 亿的比特位
     */
    private static final int DEFAULT_SIZE = 256 << 22;
 
    /**
     * 为了降低错误率,使用加法hash算法,所以定义一个8个元素的质数数组
     */
    private static final int[] seeds = {3, 5, 7, 11, 13, 31, 37, 61};
 
    /**
     * 相当于构建 8 个不同的hash算法
     */
    private static HashFunction[] functions = new HashFunction[seeds.length];
 
    /**
     * 初始化布隆过滤器的 bitmap
     */
    private static BitSet bitset = new BitSet(DEFAULT_SIZE);
 
    /**
     * 添加数据
     *
     * @param value 需要加入的值
     */
    public static void add(String value) {
        if (value != null) {
            for (HashFunction f : functions) {
                //计算 hash 值并修改 bitmap 中相应位置为 true
                bitset.set(f.hash(value), true);
            }
        }
    }
 
    /**
     * 判断相应元素是否存在
     * @param value 需要判断的元素
     * @return 结果
     */
    public static boolean contains(String value) {
        if (value == null) {
            return false;
        }
        boolean ret = true;
        for (HashFunction f : functions) {
            ret = bitset.get(f.hash(value));
            //一个 hash 函数返回 false 则跳出循环
            if (!ret) {
                break;
            }
        }
        return ret;
    }
 
    /**
     * 测试。。。
     */
    public static void main(String[] args) {
 
        for (int i = 0; i < seeds.length; i++) {
            functions[i] = new HashFunction(DEFAULT_SIZE, seeds[i]);
        }
 
        // 添加1亿数据
        for (int i = 0; i < 100000000; i++) {
            add(String.valueOf(i));
        }
        String id = "123456789";
        add(id);
 
        System.out.println(contains(id));   // true
        System.out.println("" + contains("234567890"));  //false
    }
}
 
class HashFunction {
 
    private int size;
    private int seed;
 
    public HashFunction(int size, int seed) {
        this.size = size;
        this.seed = seed;
    }
 
    public int hash(String value) {
        int result = 0;
        int len = value.length();
        for (int i = 0; i < len; i++) {
            result = seed * result + value.charAt(i);
        }
        int r = (size - 1) & result;
        return (size - 1) & result;
    }
}

2、基于框架API实现

目前java主流实现框架,有redisson和guava等等
基于redission的测试demo:

@Test
public void testBloomFilter() {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379");
    RedissonClient cient = Redisson.create(config);
    RBloomFilter<String> bloomFilter = cient.getBloomFilter("test-bloom-filter");
    // 初始化布隆过滤器,数组长度100W,误判率 1%
    bloomFilter.tryInit(1000000L, 0.01);
    // 添加数据
    bloomFilter.add("测试数据1");
    // 判断是否存在
    System.out.println(bloomFilter.contains("测试数据1"));
    System.out.println(bloomFilter.contains("测试数据2"));
}

基于guava的测试demo如下:

public static void main(String[] args){
         int capacity = 100000;
         /** 初始化容量为10万大小的字符串布隆过滤器,默认误差率为0.03
          *  布隆过滤器容量为10万并非指bitmap的长度就是10万,因为需要考虑到存在hash冲突的情况,所以bitmap的实际长度要比10万要大很多
          *  bitmap长度比需要存在的数据量大小越大,误差率会越低
          * */
         BloomFilter bloomFilter =BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), capacity);
 
         Set<String> sets = new HashSet<>();
         List<String> lists = new ArrayList<>();
 
         for (int i =0;i< capacity; i++){
             String str = UUID.randomUUID().toString();
             bloomFilter.put(str);
             sets.add(str);
             lists.add(str);
         }
 
         int existsCount = 0;
         int mightExistsCount = 0;
 
         for(int i=0;i<10000;i++){
             //如果i为100倍数,取实际的值;否则就随机一个字符串
             String data = i%100==0?lists.get(i/100):UUID.randomUUID().toString();
             /** 通过布隆过滤器判断字符串是否存在*/
             if(bloomFilter.mightContain(data)){
                 /** 如果布隆过滤器认为存在,则表示可能存在的数量mightExistsCount自增1*/
                 mightExistsCount++;
                 /** 如果set中存在则existsCount自增1*/
                 if(sets.contains(data)){
                     existsCount++;
                 }
             }
         }
 
         //测试总次数
         BigDecimal total = new BigDecimal(10000);
         //错误总次数
         BigDecimal error = new BigDecimal(mightExistsCount - existsCount);
         //误差率
         BigDecimal rate = error.divide(total, 2, BigDecimal.ROUND_HALF_UP);
 
         System.out.println("初始化10万条数据,判断100个真实数据,9900个不存在数据");
         System.out.println("实际存在的字符串个数为:" + existsCount);
         System.out.println("布隆过滤器认为存在的个数为:" + mightExistsCount);
         System.out.println("误差率为:" + rate.doubleValue());
     }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浪子城

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值