当前位置:   article > 正文

springboot基础(71):布隆过滤器的应用_springboot 布隆过滤器

springboot 布隆过滤器

前言

什么是布隆过滤器?
布隆过滤器有什么作用?
微服务中布隆过滤器的方案?

第一节 什么是布隆过滤器?

布隆过滤器(Bloom Filter)是由 Bloom 于 1970 年提出的。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的的 List、Map 、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。

在这里插入图片描述
位数组中的每个元素都只占有1 bit,并且每个元素只能是0或1。这样申请一个100万的数组只占用1000000 Bit /8= 125000 Byte = 122 kb的空间。
Bloom提出的这种用来检索元素是否在给定大集合中的数据架构,这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且理论下,添加到集合的元素越多,误报的可能性就越大。

第二节 布隆过滤器的原理

1. 当一个元素添加到布隆过滤器中的时候,会进行如下操作

1 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值
2.根据得到得哈希值,在位数组中把对应下标得位置为1

2. 当我们需要判断一个元素是否存在于布隆过滤器得时候,会进行如下操作

  1. 把给定元素再次进行相同的哈希计算
  2. 得到的值之后判断位数组的每个元素是否都为1,如果值位1,那么说明这个值在布隆过滤器中,如果存在一个值不为1,说明该元素不在布隆过滤器中。
    举个例子
    在这里插入图片描述
    不同的字符串可能哈希出来相同的位置相同,这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。
    综上,我们可以得出:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

3. 布隆过滤器的使用场景

  • 判断给定数据是否存在:比如判断一个数字是否存在于包含大量数字几种(数量很大的情况)、防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等、邮箱的垃圾邮件过滤、黑名单功能等。
  • 去重: 比如爬虫给定网址的时候对已经爬取过的URL 去重。

第三节 BloomFilter的实现方式有很多

1. 手动实现Bloom过滤器

import java.util.BitSet;

public class MyBloomFilter {

    /**
     * 位数组的大小
     */
    private static final int DEFAULT_SIZE = 2 << 24;
    /**
     * 通过这个数组可以创建 6 个不同的哈希函数
     */
    private static final int[] SEEDS = new int[]{5, 12, 23, 71, 95, 113};

    /**
     * 位数组。数组中的元素只能是 0 或者 1
     */
    private BitSet bits = new BitSet(DEFAULT_SIZE);

    /**
     * 存放包含 hash 函数的类的数组
     */
    private SimpleHash[] func = new SimpleHash[SEEDS.length];

    /**
     * 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样
     */
    public MyBloomFilter() {
        // 初始化多个不同的 Hash 函数
        for (int i = 0; i < SEEDS.length; i++) {
            func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]);
        }
    }

    /**
     * 添加元素到位数组
     */
    public void add(Object value) {
        for (SimpleHash f : func) {
            bits.set(f.hash(value), true);
        }
    }

    /**
     * 判断指定元素是否存在于位数组
     */
    public boolean contains(Object value) {
        boolean ret = true;
        for (SimpleHash f : func) {
            ret = ret && bits.get(f.hash(value));
        }
        return ret;
    }

    /**
     * 静态内部类。用于 hash 操作!
     */
    public static class SimpleHash {

        private int cap;
        private int seed;

        public SimpleHash(int cap, int seed) {
            this.cap = cap;
            this.seed = seed;
        }

        /**
         * 计算 hash 值
         */
        public int hash(Object value) {
            int h;
            return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16)));
        }

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76

测试

@Test
    public void testMyBloom(){
        String value1 = "xiaowang";
        String value2 = "xiaoliu";
        String value3 = "xiao";
        MyBloomFilter filter = new MyBloomFilter();
        System.out.println(filter.contains(value1));
        System.out.println(filter.contains(value2));
        System.out.println(filter.contains(value3));
        filter.add(value1);
        filter.add(value2);
        System.out.println(filter.contains(value1));
        System.out.println(filter.contains(value2));
        System.out.println(filter.contains(value3));
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

运行结果,可以看到布隆过滤器可以轻易的辨识指定key是否存在。
在这里插入图片描述

2. Guava实现布隆过滤器

2.1 使用Guava布隆过滤器

  1. 引入依赖
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  1. 编写示例demo
@Test
    public void testBloom() {
        /**
         *  expectedInsertions 预期插入值
         *  这个值的设置相当重要,如果设置的过小很容易导致饱和而导致误报率急剧上升,如果设置的过大,也会对内存造成浪费,所以要根据实际情况来定
         *  fpp                误差率,例如:0.01,表示误差率为1%
         */
        int expectedInsertions = 100_0000;//期望插入的数据量
        double fpp = 0.01; //误判率

        // 创建布隆过滤器对象
        BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), expectedInsertions, fpp);
        BloomFilter<Long> filter2 = BloomFilter.create(Funnels.longFunnel(), expectedInsertions, fpp);
        BloomFilter<String> filter3 = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), expectedInsertions, fpp);
        System.out.println("------------未插入数据前检测-----------");
        for(int i=899990;i<90_0003;i++){
            // 判断指定元素是否存在
            System.out.println(i+" 是否存在: "+filter.mightContain(i));
        }

        long s1=System.currentTimeMillis();
        for (int i = 0; i < 90_0000; i++) {//向过滤器添加90万数据
            // 将元素添加进布隆过滤器
            filter.put(i);
        }
        System.out.println("添加90_0000条数据耗时:"+(System.currentTimeMillis()-s1)+" ms");

        System.out.println("------------插入数据后检测-----------");
        long s2=System.currentTimeMillis();
        for(int i=899990;i<90_0003;i++){
            // 判断指定元素是否存在
            System.out.println(i+" 是否存在: "+filter.mightContain(i));
        }
        System.out.println("检测耗时:"+(System.currentTimeMillis()-s2)+" ms");
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

demo示例Bloom过滤器预期100万条记录(id),实际添加了90万条记录,设定的误差率率为1%

运行结果,可以看到它能准确的判断id是否存在。
在这里插入图片描述
在我们的示例中,当 mightContain() 方法返回 true 时,我们可以 99%确定该元素在过滤器中,当过滤器返回 false 时,我们可以 100%确定该元素不存在于过滤器中。

2.2 其它类型的数据

Long类型的id

BloomFilter<Long> filter2 = BloomFilter.create(Funnels.longFunnel(), expectedInsertions, fpp);
  • 1

String类型的内容

BloomFilter<String> filter3 = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), expectedInsertions, fpp);
  • 1

2.3 Guava的缺点

使用Guava提供的布隆过滤器,需要每个服务上都有这份数据,这就意味着在微服务之间(同一微服务的多实例)无法实现共享,各自都需要建立私有数据在内存中。为了解决这个问题,我们就需要使用到redis中的布隆过滤器。

3. 跨服务布隆过滤器方案-RedisBloom

3.1 官方文档

官方demo:https://github.com/dengliming/redis-modules-java
在这里插入图片描述

官方API:https://github.com/dengliming/redis-modules-java/tree/master/redisbloom

在这里插入图片描述

3.2 快速开始

3.2.1 安装redis-bloom

这里我们使用docker进行安装,因为操作起来比较简单。

docker run -d -p 16379:6379 --name redis-redisbloom redislabs/rebloom:latest
  • 1

相关的命令请见
https://hub.docker.com/r/redislabs/rebloom/
在这里插入图片描述

3.2.2 使用redis-bloom
  1. 导入依赖(选择一个导入即可)
  • 单独引入redisbloom
        <dependency>
            <groupId>io.github.dengliming.redismodule</groupId>
            <artifactId>redisbloom</artifactId>
            <version>2.0.1</version>
        </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 导入redismodule全部
        <!-- release -->
        <dependency>
            <groupId>io.github.dengliming.redismodule</groupId>
            <artifactId>all</artifactId>
            <version>2.0.1</version>
        </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  1. 编写demo

这里直接使用的官方demo

package com.it2.springbootbloom;

import io.github.dengliming.redismodule.redisbloom.BloomFilter;
import io.github.dengliming.redismodule.redisbloom.client.RedisBloomClient;
import org.junit.jupiter.api.Test;
import org.redisson.config.Config;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class BloomFilterTest2 {
    @Test
    public void testBloom(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://106.13.2.249:16379");//可以选择单机,也可以选择集群
        RedisBloomClient redisBloomClient = new RedisBloomClient(config);

        BloomFilter bloomFilter = redisBloomClient.getRBloomFilter("bf");
        bloomFilter.create(0.1d, 100);//预期插入值100,误差率(10%)
        bloomFilter.madd(new String[] {"a", "b", "c"});//添加数据
        System.out.println(bloomFilter.exists("a"));//a是否存在
        System.out.println(bloomFilter.exists("d"));//d是否存在

        redisBloomClient.shutdown();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

运行代码测试
在这里插入图片描述

3.3 容量

redisbloom 的容量是多少?
因为redisbloom 使用的是setbit来实现的布隆过滤器。官方文档介绍的容量是 2 32 2^{32} 232,即为512MB。
官方地址:https://redis.io/commands/setbit/

Sets or clears the bit at offset in the string value stored at key.
The bit is either set or cleared depending on value, which can be either 0 or 1.
When key does not exist, a new string value is created. The string is grown to make sure it can hold a bit at offset. The offset argument is required to be greater than or equal to 0, and smaller than 2^32 (this limits bitmaps to 512MB). When the string at key is grown, added bits are set to 0.
Warning: When setting the last possible bit (offset equal to 2^32 -1) and the string value stored at key does not yet hold a string value, or holds a small string value, Redis needs to allocate all intermediate memory which can block the server for some time. On a 2010 MacBook Pro, setting bit number 2^32 -1 (512MB allocation) takes ~300ms, setting bit number 2^30 -1 (128MB allocation) takes ~80ms, setting bit number 2^28 -1 (32MB allocation) takes ~30ms and setting bit number 2^26 -1 (8MB allocation) takes ~8ms. Note that once this first allocation is done, subsequent calls to SETBIT for the same key will not have the allocation overhead.

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/爱喝兽奶帝天荒/article/detail/859264
推荐阅读
相关标签
  

闽ICP备14008679号