布隆过滤器学习笔记

admin 2025-12-26 20:34:57 编程 来源:ZONE.CI 全球网 0 阅读模式
[TOC]


布隆过滤器

什么是布隆过滤器?

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

image-20230227194114589

位数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。这样申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 kb ≈ 122kb 的空间。

总结:一个名叫 Bloom 的人提出了一种来检索元素是否在给定大集合中的数据结构,这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大。

原理介绍

当一个元素加入布隆过滤器中的时候,会进行如下操作:

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

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

  1. 对给定元素再次进行相同的哈希计算;
  2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

image-20230227194302544

当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后将对应的位数组的下标设置为 1(当位数组初始化时,所有位置均为 0)。当第二次存储相同字符串时,因为先前的对应位置已设置为 1,所以很容易知道此值已经存在(去重非常方便)。

如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

不同的字符串可能哈希出来的位置相同,这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。

综上,我们可以得出:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

布隆过滤器使用场景

  1. 判断给定数据是否存在:比如判断一个数字是否存在于包含大量数字的数字集中(数字集很大,5 亿以上!)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等。
  2. 去重:比如爬给定网址的时候对已经爬取过的 URL 去重。

Guava 布隆过滤器

引入 Guava 的依赖:

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>28.0-jre</version>
        </dependency>
package mao;

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

/**
 * Project name(项目名称):布隆过滤器
 * Package(包名): mao
 * Class(类名): GuavaBloomFilter
 * Author(作者): mao
 * Author QQ:1296193245
 * GitHub:https://github.com/maomao124/
 * Date(创建日期): 2023/2/27
 * Time(创建时间): 20:01
 * Version(版本): 1.0
 * Description(描述): 无
 */

public class GuavaBloomFilter
{
    public static void main(String[] args)
    {
        //布隆过滤器对象,创建最多存放最多2000个整数的布隆过滤器,误判率为0.01
        BloomFilter<Integer> bloomFilter = BloomFilter.create(
                Funnels.integerFunnel(), 2000, 0.01);

        //判断是否存在
        System.out.println(bloomFilter.mightContain(100));
        System.out.println(bloomFilter.mightContain(101));
        System.out.println(bloomFilter.mightContain(102));

        //设置值
        bloomFilter.put(100);
        bloomFilter.put(101);
        bloomFilter.put(102);

        //判断是否存在
        System.out.println(bloomFilter.mightContain(100));
        System.out.println(bloomFilter.mightContain(101));
        System.out.println(bloomFilter.mightContain(102));
        System.out.println(bloomFilter.mightContain(103));
    }
}

运行结果:

false
false
false
true
true
true
false
package mao;

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

/**
 * Project name(项目名称):布隆过滤器
 * Package(包名): mao
 * Class(类名): GuavaBloomFilter2
 * Author(作者): mao
 * Author QQ:1296193245
 * GitHub:https://github.com/maomao124/
 * Date(创建日期): 2023/2/27
 * Time(创建时间): 20:12
 * Version(版本): 1.0
 * Description(描述): 测试布隆过滤器误差
 */

public class GuavaBloomFilter2
{
    public static void main(String[] args)
    {
        testGuavaBloomFilter(10000, 0.01);
        testGuavaBloomFilter(10000, 0.02);
        testGuavaBloomFilter(10000, 0.05);
        testGuavaBloomFilter(10000, 0.1);
        testGuavaBloomFilter(10000, 0.5);

        System.out.println("------");
        testGuavaBloomFilter(3000, 0.01);
        testGuavaBloomFilter(3000, 0.02);
        testGuavaBloomFilter(3000, 0.05);
        testGuavaBloomFilter(3000, 0.1);
        testGuavaBloomFilter(3000, 0.5);

        System.out.println("------");
        testGuavaBloomFilter(2000, 0.01);
        testGuavaBloomFilter(2000, 0.02);
        testGuavaBloomFilter(2000, 0.05);
        testGuavaBloomFilter(2000, 0.1);
        testGuavaBloomFilter(2000, 0.5);

        System.out.println("------");
        testGuavaBloomFilter(1000, 0.01);
        testGuavaBloomFilter(1000, 0.02);
        testGuavaBloomFilter(1000, 0.05);
        testGuavaBloomFilter(1000, 0.1);
        testGuavaBloomFilter(1000, 0.5);
        testGuavaBloomFilter(1000, 0.005);
        testGuavaBloomFilter(1000, 0.001);
        testGuavaBloomFilter(1000, 0.0001);

        System.out.println("------");
        testGuavaBloomFilter(500, 0.01);
        testGuavaBloomFilter(500, 0.02);
        testGuavaBloomFilter(500, 0.05);
        testGuavaBloomFilter(500, 0.1);
        testGuavaBloomFilter(500, 0.5);
        testGuavaBloomFilter(500, 0.005);
        testGuavaBloomFilter(500, 0.001);
        testGuavaBloomFilter(500, 0.0001);

        System.out.println("------");
        testGuavaBloomFilter(200, 0.01);
        testGuavaBloomFilter(200, 0.02);
        testGuavaBloomFilter(200, 0.05);
        testGuavaBloomFilter(200, 0.1);
        testGuavaBloomFilter(200, 0.5);
        testGuavaBloomFilter(200, 0.005);
        testGuavaBloomFilter(200, 0.001);
        testGuavaBloomFilter(200, 0.0001);

        System.out.println("------");
        testGuavaBloomFilter(100, 0.01);
        testGuavaBloomFilter(100, 0.02);
        testGuavaBloomFilter(100, 0.05);
        testGuavaBloomFilter(100, 0.1);
        testGuavaBloomFilter(100, 0.5);
        testGuavaBloomFilter(100, 0.005);
        testGuavaBloomFilter(100, 0.001);
        testGuavaBloomFilter(100, 0.0001);
    }


    /**
     * 测试Guava的布隆过滤器
     *
     * @param expectedInsertions 布隆过滤器最多存放的数量
     * @param fpp                误差
     */
    public static void testGuavaBloomFilter(int expectedInsertions, double fpp)
    {
        //布隆过滤器对象,创建最多存放最多2000个整数的布隆过滤器,误判率为0.01
        BloomFilter<Integer> bloomFilter = BloomFilter.create(
                Funnels.integerFunnel(), expectedInsertions, fpp);

        //1500次循环
        for (int i = 0; i < 1500; i++)
        {
            if (i % 3 == 0)
            {
                continue;
            }
            //将i的值% 3 不等于 0 的值放进去
            bloomFilter.put(i);
        }

        //存在的计数
        int count = 0;
        //统计
        for (int i = 0; i < 1500; i++)
        {
            //判断是否存在
            boolean b = bloomFilter.mightContain(i);
            //System.out.println(i + " --> " + b);
            //可能存在
            if (b)
            {
                count++;
            }
        }
        System.out.println("最大数量:" + expectedInsertions + "  误差:" + fpp);
        System.out.println("预期结果:1000,最终结果:" + count);
        System.out.println();
    }
}

运行结果:

最大数量:10000  误差:0.01
预期结果:1000,最终结果:1000

最大数量:10000  误差:0.02
预期结果:1000,最终结果:1000

最大数量:10000  误差:0.05
预期结果:1000,最终结果:1000

最大数量:10000  误差:0.1
预期结果:1000,最终结果:1000

最大数量:10000  误差:0.5
预期结果:1000,最终结果:1030

------
最大数量:3000  误差:0.01
预期结果:1000,最终结果:1000

最大数量:3000  误差:0.02
预期结果:1000,最终结果:1000

最大数量:3000  误差:0.05
预期结果:1000,最终结果:1001

最大数量:3000  误差:0.1
预期结果:1000,最终结果:1001

最大数量:3000  误差:0.5
预期结果:1000,最终结果:1095

------
最大数量:2000  误差:0.01
预期结果:1000,最终结果:1000

最大数量:2000  误差:0.02
预期结果:1000,最终结果:1000

最大数量:2000  误差:0.05
预期结果:1000,最终结果:1002

最大数量:2000  误差:0.1
预期结果:1000,最终结果:1013

最大数量:2000  误差:0.5
预期结果:1000,最终结果:1137

------
最大数量:1000  误差:0.01
预期结果:1000,最终结果:1006

最大数量:1000  误差:0.02
预期结果:1000,最终结果:1011

最大数量:1000  误差:0.05
预期结果:1000,最终结果:1021

最大数量:1000  误差:0.1
预期结果:1000,最终结果:1053

最大数量:1000  误差:0.5
预期结果:1000,最终结果:1230

最大数量:1000  误差:0.005
预期结果:1000,最终结果:1000

最大数量:1000  误差:0.001
预期结果:1000,最终结果:1001

最大数量:1000  误差:1.0E-4
预期结果:1000,最终结果:1000

------
最大数量:500  误差:0.01
预期结果:1000,最终结果:1075

最大数量:500  误差:0.02
预期结果:1000,最终结果:1106

最大数量:500  误差:0.05
预期结果:1000,最终结果:1128

最大数量:500  误差:0.1
预期结果:1000,最终结果:1175

最大数量:500  误差:0.5
预期结果:1000,最终结果:1375

最大数量:500  误差:0.005
预期结果:1000,最终结果:1060

最大数量:500  误差:0.001
预期结果:1000,最终结果:1037

最大数量:500  误差:1.0E-4
预期结果:1000,最终结果:1011

------
最大数量:200  误差:0.01
预期结果:1000,最终结果:1423

最大数量:200  误差:0.02
预期结果:1000,最终结果:1438

最大数量:200  误差:0.05
预期结果:1000,最终结果:1421

最大数量:200  误差:0.1
预期结果:1000,最终结果:1442

最大数量:200  误差:0.5
预期结果:1000,最终结果:1479

最大数量:200  误差:0.005
预期结果:1000,最终结果:1399

最大数量:200  误差:0.001
预期结果:1000,最终结果:1375

最大数量:200  误差:1.0E-4
预期结果:1000,最终结果:1293

------
最大数量:100  误差:0.01
预期结果:1000,最终结果:1497

最大数量:100  误差:0.02
预期结果:1000,最终结果:1500

最大数量:100  误差:0.05
预期结果:1000,最终结果:1497

最大数量:100  误差:0.1
预期结果:1000,最终结果:1497

最大数量:100  误差:0.5
预期结果:1000,最终结果:1494

最大数量:100  误差:0.005
预期结果:1000,最终结果:1498

最大数量:100  误差:0.001
预期结果:1000,最终结果:1498

最大数量:100  误差:1.0E-4
预期结果:1000,最终结果:1481

mightContain() 方法返回 true 时,我们可以 99%确定该元素在过滤器中,当过滤器返回 false 时,我们可以 100%确定该元素不存在于过滤器中。

Guava 提供的布隆过滤器的实现还是很不错的,但是它有一个重大的缺陷就是只能单机使用(另外,容量扩展也不容易),而现在互联网一般都是分布式的场景。为了解决这个问题,我们就需要用到 Redis 中的布隆过滤器了。

Redis 中的布隆过滤器

介绍

Redis v4.0 之后有了 Module(模块/插件) 功能,Redis Modules 让 Redis 可以使用外部模块扩展其功能 。布隆过滤器就是其中的 Module。

https://redis.io/resources/modules/

另外,官网推荐了一个 RedisBloom 作为 Redis 布隆过滤器的 Module,地址:https://github.com/RedisBloom/RedisBloom 其他还有:

  • redis-lua-scaling-bloom-filter(lua 脚本实现):https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter
  • pyreBloom(Python 中的快速 Redis 布隆过滤器) :https://github.com/seomoz/pyreBloom

使用 Docker 安装

库:redislabs/rebloom - Docker Image | Docker Hub

第一步:搜索

命令:docker search redislabs/rebloom

PS C:\Users\mao\Desktop> docker search redislabs/rebloom
NAME                DESCRIPTION                                 STARS     OFFICIAL   AUTOMATED
redislabs/rebloom   A probablistic datatypes module for Redis   22                   [OK]
PS C:\Users\mao\Desktop>

第二步:拉取镜像

命令:docker pull redislabs/rebloom

PS C:\Users\mao\Desktop> docker pull redislabs/rebloom
Using default tag: latest
latest: Pulling from redislabs/rebloom
284055322776: Pull complete
b7286e96019c: Pull complete
8d789d3c47a4: Pull complete
3806fdc99c73: Pull complete
e751a0826f38: Pull complete
ef5499a774c1: Pull complete
c72d4acf12b3: Pull complete
38d98e95a943: Pull complete
4a50810bd519: Pull complete
209cee60b260: Pull complete
605e8f893c4b: Pull complete
Digest: sha256:182ba5c55c8b2b5758edda4682f8f7a3711efe025d593583ecf1bf3ed9b4f55e
Status: Downloaded newer image for redislabs/rebloom:latest
docker.io/redislabs/rebloom:latest
PS C:\Users\mao\Desktop>

第三步:查看镜像是否拉取成功

命令:docker images

PS C:\Users\mao\Desktop> docker images
REPOSITORY          TAG       IMAGE ID       CREATED         SIZE
redislabs/rebloom   latest    66d626dc1387   15 months ago   147MB
PS C:\Users\mao\Desktop>

第四步:运行

命令:docker run -p 16379:6379 -d --name redis-redisbloom redislabs/rebloom

PS C:\Users\mao> docker run -p 16379:6379  -d --name redis-redisbloom redislabs/rebloom
0a3197e86f21545c46e581d1b64aca3aa6a641144b9e38518fc50f8cedca1917
PS C:\Users\mao>

第五步:查看是否运行成功

命令:docker ps

PS C:\Users\mao> docker ps
CONTAINER ID   IMAGE               COMMAND                  CREATED          STATUS          PORTS                     NAMES
0a3197e86f21   redislabs/rebloom   "docker-entrypoint.s…"   14 seconds ago   Up 13 seconds   0.0.0.0:16379->6379/tcp   redis-redisbloom
PS C:\Users\mao>

第六步:进入容器

命令:docker exec -it redis-redisbloom /bin/bash

PS C:\Users\mao> docker exec -it redis-redisbloom /bin/bash
root@0a3197e86f21:/data# ls -l
total 0
root@0a3197e86f21:/data# pwd
/data
root@0a3197e86f21:/data# cd ..
root@0a3197e86f21:/#

第七步:使用redis-cli链接redis

命令:redis-cli

root@0a3197e86f21:/# redis-cli
127.0.0.1:6379> ping
PONG
127.0.0.1:6379>

如果是外部机,则需要使用命令:redis-cli -p 16379

PS C:\Users\mao> redis-cli -p 16379
127.0.0.1:16379> ping
PONG
127.0.0.1:16379>

常用命令

key : 布隆过滤器的名称,item : 添加的元素

BF.ADD

将元素添加到布隆过滤器中,如果该过滤器尚不存在,则创建该过滤器。格式:BF.ADD {key} {item}

127.0.0.1:6379> BF.ADD filter 1
(integer) 1
127.0.0.1:6379> BF.ADD filter 2
(integer) 1
127.0.0.1:6379> BF.ADD filter 3
(integer) 1
127.0.0.1:6379> BF.ADD filter 5
(integer) 1
127.0.0.1:6379>

BF.MADD

将一个或多个元素添加到“布隆过滤器”中,并创建一个尚不存在的过滤器。该命令的操作方式BF.ADD与之相同,只不过它允许多个输入并返回多个值。格式:BF.MADD {key} {item} [item ...]

127.0.0.1:6379> BF.MADD filter2 1 2 7 9 10
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 1
5) (integer) 1
127.0.0.1:6379> BF.MADD filter2 2 8
1) (integer) 0
2) (integer) 1
127.0.0.1:6379>

BF.EXISTS

确定元素是否在布隆过滤器中存在。格式:BF.EXISTS {key} {item}

127.0.0.1:6379> BF.EXISTS filter 1
(integer) 1
127.0.0.1:6379> BF.EXISTS filter 2
(integer) 1
127.0.0.1:6379> BF.EXISTS filter 3
(integer) 1
127.0.0.1:6379> BF.EXISTS filter 4
(integer) 0
127.0.0.1:6379> BF.EXISTS filter 5
(integer) 1
127.0.0.1:6379> BF.EXISTS filter2 5
(integer) 0
127.0.0.1:6379> BF.EXISTS filter2 7
(integer) 1
127.0.0.1:6379> BF.EXISTS filter2 8
(integer) 1
127.0.0.1:6379> BF.EXISTS filter2 3
(integer) 0
127.0.0.1:6379>

BF.MEXISTS

确定一个或者多个元素是否在布隆过滤器中存在。格式:BF.MEXISTS {key} {item} [item ...]

127.0.0.1:6379> BF.MEXISTS filter 1 2 3 4 5 6 7 8 9 10 11
 1) (integer) 1
 2) (integer) 1
 3) (integer) 1
 4) (integer) 0
 5) (integer) 1
 6) (integer) 0
 7) (integer) 0
 8) (integer) 0
 9) (integer) 0
10) (integer) 0
11) (integer) 0
127.0.0.1:6379>
127.0.0.1:6379> BF.MEXISTS filter2 1 2 3 4 5 6 7 8 9 10 11
 1) (integer) 1
 2) (integer) 1
 3) (integer) 0
 4) (integer) 0
 5) (integer) 0
 6) (integer) 0
 7) (integer) 1
 8) (integer) 1
 9) (integer) 1
10) (integer) 1
11) (integer) 0
127.0.0.1:6379>

BF. RESERVE

命令的格式如下:

BF. RESERVE {key} {error_rate} {capacity} [EXPANSION expansion]

参数的具体含义:

  1. key:布隆过滤器的名称
  2. error_rate : 期望的误报率。该值必须介于 0 到 1 之间。例如,对于期望的误报率 0.1%(1000 中为 1),error_rate 应该设置为 0.001。该数字越接近零,则每个项目的内存消耗越大,并且每个操作的 CPU 使用率越高。
  3. capacity: 过滤器的容量。当实际存储的元素个数超过这个值之后,性能将开始下降。实际的降级将取决于超出限制的程度。随着过滤器元素数量呈指数增长,性能将线性下降。

可选参数:

  • expansion:如果创建了一个新的子过滤器,则其大小将是当前过滤器的大小乘以expansion。默认扩展值为 2。这意味着每个后续子过滤器将是前一个子过滤器的两倍。
127.0.0.1:6379> BF.RESERVE filter3 0.001 500
OK
127.0.0.1:6379>

Java操作Redis布隆过滤器

使用RESP2.0协议来操作redis服务端

RedisInformation类

package mao;

import java.io.InputStream;
import java.util.Properties;

/**
 * Project name(项目名称):布隆过滤器
 * Package(包名): mao
 * Class(类名): RedisInformation
 * Author(作者): mao
 * Author QQ:1296193245
 * GitHub:https://github.com/maomao124/
 * Date(创建日期): 2023/2/27
 * Time(创建时间): 21:59
 * Version(版本): 1.0
 * Description(描述): redis服务的信息对象
 */

public class RedisInformation
{

    /**
     * redis服务的ip
     */
    private static final String host;
    /**
     * redis服务的端口号
     */
    private static final int port;
    /**
     * redis服务的密码
     */
    private static final String password;

    /**
     * 单行字符串
     */
    public static final char SINGLE_LINE_STRING = '+';
    /**
     * 异常或者错误
     */
    public static final char ERROR = '-';
    /**
     * 数值
     */
    public static final char NUMBER = ':';
    /**
     * 多行字符串
     */
    public static final char MULTILINE_STRING = '$';
    /**
     * 数组
     */
    public static final char ARRAY = '*';


    static
    {
        try
        {
            //从类路径里获取配置信息
            InputStream inputStream = RedisInformation.class.getClassLoader().getResourceAsStream("redis.properties");
            Properties properties = new Properties();
            //加载配置到properties
            properties.load(inputStream);
            //ip地址
            host = properties.getProperty("redis.host");
            //端口号
            port = Integer.parseInt(properties.getProperty("redis.port"));
            //密码
            password = properties.getProperty("redis.password");
        }
        catch (Exception e)
        {
            throw new RuntimeException(e);
        }
    }


    public static String getHost()
    {
        return host;
    }

    public static int getPort()
    {
        return port;
    }

    public static String getPassword()
    {
        return password;
    }
}

定义接口RedisBloomFilter

package mao;

import java.util.List;

/**
 * Project name(项目名称):布隆过滤器
 * Package(包名): mao
 * Interface(接口名): RedisBloomFilter
 * Author(作者): mao
 * Author QQ:1296193245
 * GitHub:https://github.com/maomao124/
 * Date(创建日期): 2023/2/27
 * Time(创建时间): 22:05
 * Version(版本): 1.0
 * Description(描述): redis布隆过滤器客户端
 */

public interface RedisBloomFilter
{
    /**
     * 关闭链接
     */
    void close();

    /**
     * 将元素添加到布隆过滤器中,如果该过滤器尚不存在,则创建该过滤器。
     *
     * @param filterKey 布隆过滤器的名称
     * @param item      添加的元素
     * @return boolean
     */
    boolean add(String filterKey, String item);

    /**
     * 将一个或多个元素添加到“布隆过滤器”中,并创建一个尚不存在的过滤器。
     * 该命令的操作方式`BF.ADD`与之相同,只不过它允许多个输入并返回多个值。
     *
     * @param filterKey 布隆过滤器的名称
     * @param items     添加的元素
     * @return {@link List}<{@link Boolean}>
     */
    List<Boolean> mAdd(String filterKey, String... items);


    /**
     * 确定元素是否在布隆过滤器中存在
     *
     * @param filterKey 布隆过滤器的名称
     * @param item      添加的元素
     * @return boolean 如果存在,则为true,反之为false
     */
    boolean exists(String filterKey, String item);


    /**
     * 确定一个或者多个元素是否在布隆过滤器中存在
     * 该命令的操作方式`BF.EXISTS`与之相同,只不过它允许多个输入并返回多个值。
     *
     * @param filterKey 布隆过滤器的名称
     * @param items     添加的元素
     * @return {@link List}<{@link Boolean}>
     */
    List<Boolean> mExists(String filterKey, String... items);


    /**
     * 储备
     *
     * @param filterKey  布隆过滤器的名称
     * @param error_rate 期望的误报率。该值必须介于 0 到 1 之间。例如,对于期望的误报率 0.1%(1000 中为 1),
     *                   error_rate 应该设置为 0.001。
     *                   该数字越接近零,则每个项目的内存消耗越大,并且每个操作的 CPU 使用率越高。
     * @param capacity   过滤器的容量。当实际存储的元素个数超过这个值之后,性能将开始下降。
     *                   实际的降级将取决于超出限制的程度。随着过滤器元素数量呈指数增长,性能将线性下降。
     * @return boolean
     */
    boolean reserve(String filterKey, float error_rate, int capacity);
}

实现接口

package mao;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * Project name(项目名称):布隆过滤器
 * Package(包名): mao
 * Class(类名): RedisBloomFilterImpl
 * Author(作者): mao
 * Author QQ:1296193245
 * GitHub:https://github.com/maomao124/
 * Date(创建日期): 2023/2/27
 * Time(创建时间): 22:05
 * Version(版本): 1.0
 * Description(描述): redis布隆过滤器客户端接口实现类
 */

public class RedisBloomFilterImpl implements RedisBloomFilter
{
    private final Socket socket;
    private final PrintWriter printWriter;
    private final BufferedReader bufferedReader;


    /**
     * Instantiates a new Redis client.
     */
    public RedisBloomFilterImpl()
    {
        try
        {
            //连接redis
            socket = new Socket(RedisInformation.getHost(), RedisInformation.getPort());
            //获取输入流
            bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
            //获取输出流
            printWriter = new PrintWriter(socket.getOutputStream());
            //身份认证
            if (RedisInformation.getPassword() != null)
            {
                sendRequest("auth", RedisInformation.getPassword());
                Object response = getResponse();
                System.out.println("密码验证成功");
            }
        }
        catch (IOException e)
        {
            throw new RuntimeException(e);
        }
    }

    /**
     * Instantiates a new Redis client.
     *
     * @param host     the host
     * @param port     the port
     * @param password the password
     */
    public RedisBloomFilterImpl(String host, int port, String password)
    {
        try
        {
            //连接redis
            socket = new Socket(host, port);
            //获取输入流
            bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
            //获取输出流
            printWriter = new PrintWriter(socket.getOutputStream());
            //身份认证
            if (password != null)
            {
                sendRequest("auth", password);
                Object response = getResponse();
                System.out.println("密码验证成功:" + response);
            }
        }
        catch (IOException e)
        {
            throw new RuntimeException(e);
        }
    }

    /**
     * Instantiates a new Redis client.
     *
     * @param host the host
     * @param port the port
     */
    public RedisBloomFilterImpl(String host, int port)
    {
        try
        {
            //连接redis
            socket = new Socket(host, port);
            //获取输入流
            bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
            //获取输出流
            printWriter = new PrintWriter(socket.getOutputStream());
        }
        catch (IOException e)
        {
            throw new RuntimeException(e);
        }
    }


    /**
     * 关闭redis客户端与redis服务端的连接
     */
    public void close()
    {
        try
        {
            if (printWriter != null)
            {
                printWriter.close();
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        try
        {
            if (bufferedReader != null)
            {
                bufferedReader.close();
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        try
        {
            if (socket != null)
            {
                socket.close();
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }

    }

    /**
     * 发送请求
     *
     * @param args 发起请求的参数,参数数量不一定
     */
    private void sendRequest(String... args)
    {
        //先写入元素个数,数组,换行
        printWriter.println("*" + args.length);
        //剩余的都是数组,遍历添加
        for (String arg : args)
        {
            //$为多行字符串,长度
            printWriter.println("$" + arg.getBytes(StandardCharsets.UTF_8).length);
            printWriter.println(arg);
        }
        //刷新
        printWriter.flush();
    }

    /**
     * 获取发送请求后的响应
     *
     * @return Object对象
     */
    private Object getResponse()
    {
        try
        {
            //获取当前前缀,因为要判断是什么类型
            char prefix = (char) bufferedReader.read();
            //判断是什么类型
            if (prefix == RedisInformation.SINGLE_LINE_STRING)
            {
                //单行字符串
                //直接读一行,读到换行符
                return bufferedReader.readLine();
            }
            if (prefix == RedisInformation.ERROR)
            {
                //错误
                //抛出运行时异常
                throw new RuntimeException(bufferedReader.readLine());
            }
            if (prefix == RedisInformation.NUMBER)
            {
                //数值
                //转数字
                return Integer.valueOf(bufferedReader.readLine());
            }
            if (prefix == RedisInformation.MULTILINE_STRING)
            {
                //多行字符串
                //先获取长度
                int length = Integer.parseInt(bufferedReader.readLine());
                //判断数组是否为空
                if (length == -1 || length == 0)
                {
                    //不存在或者数组为空
                    //返回空字符串
                    return "";
                }
                //不为空,读取
                return bufferedReader.readLine();
            }
            if (prefix == RedisInformation.ARRAY)
            {
                //数组
                return readBulkString();
            }
        }
        catch (Exception e)
        {
            throw new RuntimeException(e);
        }
        return null;
    }

    /**
     * 读取数组
     *
     * @return List<Object>
     * @throws IOException IOException
     */
    private List<Object> readBulkString() throws IOException
    {
        //获取当前数组的大小
        int size = Integer.parseInt(bufferedReader.readLine());
        //判断数组大小
        if (size == 0 || size == -1)
        {
            //返回null
            return null;
        }
        //数组有值
        //构建集合
        List<Object> list = new ArrayList<>(3);
        //遍历获取
        for (int i = 0; i < size; i++)
        {
            try
            {
                //递归获取
                list.add(getResponse());
            }
            catch (Exception e)
            {
                //异常加入到集合
                list.add(e);
            }
        }
        //返回
        return list;
    }

    /**
     * 将元素添加到布隆过滤器中,如果该过滤器尚不存在,则创建该过滤器。
     *
     * @param filterKey 布隆过滤器的名称
     * @param item      添加的元素
     * @return boolean
     */
    public boolean add(String filterKey, String item)
    {
        //发送命令
        this.sendRequest("BF.ADD", filterKey, item);
        //读取结果
        String response = Objects.requireNonNull(this.getResponse()).toString();
        if (Objects.equals(response, "1"))
        {
            return true;
        }
        if (Objects.equals(response, "0"))
        {
            return false;
        }
        throw new RuntimeException(Objects.requireNonNull(response).toString());
    }


    /**
     * 将一个或多个元素添加到“布隆过滤器”中,并创建一个尚不存在的过滤器。
     * 该命令的操作方式`BF.ADD`与之相同,只不过它允许多个输入并返回多个值。
     *
     * @param filterKey 布隆过滤器的名称
     * @param items     添加的元素
     * @return {@link List}<{@link Boolean}>
     */
    public List<Boolean> mAdd(String filterKey, String... items)
    {
        if (filterKey == null)
        {
            return null;
        }

        String[] args = new String[items.length + 2];
        args[0] = "BF.MADD";
        args[1] = filterKey;
        System.arraycopy(items, 0, args, 2, items.length);
        sendRequest(args);
        String r = getResponse().toString();
        r = r.substring(1, r.length() - 1);
        String[] split = r.split(", ");
        List<Boolean> list = new ArrayList<>(items.length);
        for (String s : split)
        {
            if (Objects.equals(s, "1"))
            {
                list.add(true);
                continue;
            }
            if (Objects.equals(s, "0"))
            {
                list.add(false);
                continue;
            }
            throw new RuntimeException(Objects.requireNonNull(s));
        }
        return list;
    }


    /**
     * 确定元素是否在布隆过滤器中存在
     *
     * @param filterKey 布隆过滤器的名称
     * @param item      添加的元素
     * @return boolean 如果存在,则为true,反之为false
     */
    public boolean exists(String filterKey, String item)
    {
        //发送命令
        this.sendRequest("BF.EXISTS", filterKey, item);
        //读取结果
        String response = Objects.requireNonNull(this.getResponse()).toString();
        if (Objects.equals(response, "1"))
        {
            return true;
        }
        if (Objects.equals(response, "0"))
        {
            return false;
        }
        throw new RuntimeException(Objects.requireNonNull(response));
    }


    /**
     * 确定一个或者多个元素是否在布隆过滤器中存在
     * 该命令的操作方式`BF.EXISTS`与之相同,只不过它允许多个输入并返回多个值。
     *
     * @param filterKey 布隆过滤器的名称
     * @param items     添加的元素
     * @return {@link List}<{@link Boolean}>
     */
    public List<Boolean> mExists(String filterKey, String... items)
    {
        if (filterKey == null)
        {
            return null;
        }

        String[] args = new String[items.length + 2];
        args[0] = "BF.MEXISTS";
        args[1] = filterKey;
        System.arraycopy(items, 0, args, 2, items.length);
        sendRequest(args);
        String r = getResponse().toString();
        r = r.substring(1, r.length() - 1);
        String[] split = r.split(", ");
        List<Boolean> list = new ArrayList<>(items.length);
        for (String s : split)
        {
            if (Objects.equals(s, "1"))
            {
                list.add(true);
                continue;
            }
            if (Objects.equals(s, "0"))
            {
                list.add(false);
                continue;
            }
            throw new RuntimeException(Objects.requireNonNull(s));
        }
        return list;
    }


    /**
     * 储备
     *
     * @param filterKey  布隆过滤器的名称
     * @param error_rate 期望的误报率。该值必须介于 0 到 1 之间。例如,对于期望的误报率 0.1%(1000 中为 1),
     *                   error_rate 应该设置为 0.001。
     *                   该数字越接近零,则每个项目的内存消耗越大,并且每个操作的 CPU 使用率越高。
     * @param capacity   过滤器的容量。当实际存储的元素个数超过这个值之后,性能将开始下降。
     *                   实际的降级将取决于超出限制的程度。随着过滤器元素数量呈指数增长,性能将线性下降。
     * @return boolean
     */
    public boolean reserve(String filterKey, float error_rate, int capacity)
    {
        if (error_rate > 1 || error_rate < 0)
        {
            throw new RuntimeException("期望的误报率应该在0到1之间");
        }
        if (capacity <= 0)
        {
            throw new RuntimeException("过滤器的容量必须要大于0");
        }
        //发送命令
        this.sendRequest("BF.RESERVE", filterKey, String.valueOf(error_rate), String.valueOf(capacity));
        //读取结果
        String response = Objects.requireNonNull(this.getResponse()).toString();
        if (Objects.equals(response, "OK"))
        {
            return true;
        }
        throw new RuntimeException(Objects.requireNonNull(response));
    }


    public static void main(String[] args)
    {
        RedisBloomFilterImpl bloomFilter = new RedisBloomFilterImpl("127.0.0.1", 16379);
        bloomFilter.sendRequest("BF.ADD", "filter", "1");
        System.out.println(bloomFilter.getResponse());
        bloomFilter.sendRequest("BF.MADD", "filter", "1", "2", "3");
        String s = bloomFilter.getResponse().toString();
        s = s.substring(1, s.length() - 1);
        System.out.println(s);
        String[] split = s.split(", ");
        for (String s1 : split)
        {
            System.out.println(s1);
        }

        bloomFilter.sendRequest("BF.EXISTS", "filter", "1");
        System.out.println(bloomFilter.getResponse());
    }
}

单元测试

package mao;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Project name(项目名称):布隆过滤器
 * Package(包名): mao
 * Class(测试类名): RedisBloomFilterImplTest
 * Author(作者): mao
 * Author QQ:1296193245
 * GitHub:https://github.com/maomao124/
 * Date(创建日期): 2023/2/27
 * Time(创建时间): 22:36
 * Version(版本): 1.0
 * Description(描述): 测试类
 */

class RedisBloomFilterImplTest
{

    private static RedisBloomFilterImpl redisBloomFilter;

    @BeforeAll
    static void beforeAll()
    {
        redisBloomFilter = new RedisBloomFilterImpl("127.0.0.1", 16379);
    }

    @AfterAll
    static void afterAll()
    {
        redisBloomFilter.close();
    }

    @Test
    void add()
    {
        System.out.println(redisBloomFilter.add("filter3", "1"));
        System.out.println(redisBloomFilter.add("filter3", "2"));
        System.out.println(redisBloomFilter.add("filter3", "3"));
        System.out.println(redisBloomFilter.add("filter3", "4"));
        System.out.println(redisBloomFilter.add("filter3", "5"));

    }

    @Test
    void mAdd()
    {
        List<Boolean> filter4 = redisBloomFilter.mAdd("filter4", "1", "3", "4", "6");
        System.out.println(filter4);
        List<Boolean> filter44 = redisBloomFilter.mAdd("filter4", "1", "3", "4", "7");
        System.out.println(filter44);
    }

    @Test
    void exists()
    {
        System.out.println(redisBloomFilter.exists("filter3", "1"));
        System.out.println(redisBloomFilter.exists("filter3", "2"));
        System.out.println(redisBloomFilter.exists("filter3", "4"));
        System.out.println(redisBloomFilter.exists("filter3", "5"));
        System.out.println(redisBloomFilter.exists("filter3", "6"));
        System.out.println(redisBloomFilter.exists("filter3", "9"));

        System.out.println(redisBloomFilter.exists("filter4", "6"));
        System.out.println(redisBloomFilter.exists("filter4", "7"));
        System.out.println(redisBloomFilter.exists("filter4", "9"));
    }

    @Test
    void mExists()
    {
        List<Boolean> filter3 = redisBloomFilter.mExists
                ("filter3", "1", "2", "3", "4", "5", "6", "7", "8");
        System.out.println(filter3);

        List<Boolean> filter4 = redisBloomFilter.mExists(
                "filter4", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"
        );
        System.out.println(filter4);
    }


    @Test
    void reserve()
    {
        System.out.println(redisBloomFilter.reserve("filter5", 0.002f, 2000));
        System.out.println(redisBloomFilter.reserve("filter6", 0.002f, 2000));
    }
}

测试误差

package mao;

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

/**
 * Project name(项目名称):布隆过滤器
 * Package(包名): mao
 * Class(类名): RedisBloomFilterTest
 * Author(作者): mao
 * Author QQ:1296193245
 * GitHub:https://github.com/maomao124/
 * Date(创建日期): 2023/2/27
 * Time(创建时间): 23:13
 * Version(版本): 1.0
 * Description(描述): 测试redis的布隆过滤器,因为有网络io,所以很慢
 */

public class RedisBloomFilterTest
{
    public static void main(String[] args)
    {
        //redis的布隆过滤器
        RedisBloomFilter redisBloomFilter = new RedisBloomFilterImpl("127.0.0.1", 16379);

        //1500次循环
        for (int i = 0; i < 1500; i++)
        {
            if (i % 3 == 0)
            {
                continue;
            }
            //将i的值% 3 不等于 0 的值放进去
            redisBloomFilter.add("filter11", String.valueOf(i));
        }

        //存在的计数
        int count = 0;
        //统计
        for (int i = 0; i < 1500; i++)
        {
            //判断是否存在
            boolean b = redisBloomFilter.exists("filter11", String.valueOf(i));
            //System.out.println(i + " --> " + b);
            //可能存在
            if (b)
            {
                count++;
            }
        }

        System.out.println("预期结果:1000,最终结果:" + count);
        System.out.println();

        redisBloomFilter.close();
    }
}

end


by mao 2023 02 27