Springboot RedisBlooom & BitSet & Guava BloomFilter 布隆过滤分析

·  阅读 914

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

1:使用 BloomFilter 背景

BloomFilter在项目开发过程中经常使用,比如缓存穿透、爬虫过滤、猜你喜欢多路召回过滤等,判断一个或多个元素是否在一个集合内。它的空间效率和查询时间都远远超过一般的算法,当然它的缺点是有一定的误识别率和删除困难。基于性能和内存使用上考虑,最终选择BloomFilter。接下来重点梳理一下bloomfilter如何使用以及实现原理。

2:BloomFilter 单机使用

1:guava BloomFilter

其核心是hash 函数的选取以及 bit 数组的大小,主要使用:MURMUR128_MITZ_32 和 MURMUR128_MITZ_64,两者使用的都是MurmurHash3算法。详细的实现见guava的实现类:BloomFilterStrategies ,详细底层算法实现见:Bloom_filter算法分析

## 引入guava  maven 库
<dependency>
	  <groupId>com.google.guava</groupId>
	  <artifactId>guava</artifactId>
      <version>30.1.1-jre</version>
</dependency>
复制代码

guava 使用 bloomFilter 比较简单。引入maven包,几行代码搞定。详细如下:

BloomFilter<CharSequence> bloomFilter  = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),1000,0.0000001);
bloomFilter.put("abc");
boolean  isContains = bloomFilter.mightContain("abc");
System.out.println(isContains );
复制代码

2:基于JVM的 BitSet  BloomFilter

BitSet只面向数字比较,并且必须是正数。其他类型需要先转换成int类型,转换过程中难免会出现重复,BitSet的准确性就会降低。详细如下:

public class BloomFilter {
    private BitSet bits;
    private int size;
    private AtomicInteger realSize = new AtomicInteger(0);
    private int addedElements;
    private int hashFunctionNumber;

    /**
     * 构造一个布隆过滤器,过滤器的容量为c * n 个bit.
     *
     * @param c 当前过滤器预先开辟的最大包含记录,通常要比预计存入的记录多一倍.
     * @param n 当前过滤器预计所要包含的记录.
     * @param k 哈希函数的个数,等同每条记录要占用的bit数.
     */
    public BloomFilter(int c, int n, int k) {
        if (k > 8) {
            throw new IllegalArgumentException("Illegal k(maximum is 8): " + k);
        }
        this.hashFunctionNumber = k;
        this.size = (int) Math.ceil(c * k);
        this.addedElements = n;
        this.bits = new BitSet(size);
    }

    /**
     * 写入Bloom过滤器
     *
     * @param str 缓存字符串
     */
    public void put(String str) {
        realSize.incrementAndGet();
        byte[] bytes = str.getBytes();
        int[] positions = createHashes(bytes, hashFunctionNumber);
        for (int i : positions) {
            int position = Math.abs(i % size);
            bits.set(position, true);
        }
    }

    /**
     * Bloom过滤器是否包含对应的字符串
     *
     * @param str 字符串对象
     * @return true表示包含
     */
    public boolean contains(String str) {
        byte[] bytes = str.getBytes();
        int[] positions = createHashes(bytes, hashFunctionNumber);
        for (int i : positions) {
            int position = Math.abs(i % size);
            if (!bits.get(position)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 得到当前过滤器的错误率.
     *
     * @return 错误率
     */
    public double getFalsePositiveProbability() {
        return Math.pow((1 - Math.exp(-hashFunctionNumber * (double) addedElements / size)), hashFunctionNumber);
    }

    public int getSize() {
        return size;
    }

    public int getRealSize() {
        return realSize.get();
    }

    private int[] createHashes(byte[] bytes, int hashFunctionNumber) {
        int[] result = new int[hashFunctionNumber];
        for (int i = 0; i < hashFunctionNumber; i++) {
            result[i] = HashFunctions.hash(bytes, i);
        }
        return result;
    }

    static class HashFunctions {
        static int hash(byte[] bytes, int index) {
            switch (index) {
                case 0:
                    return RSHash(bytes);
                case 1:
                    return JSHash(bytes);
                case 2:
                    return ELFHash(bytes);
                case 3:
                    return BKDRHash(bytes);
                case 4:
                    return APHash(bytes);
                case 5:
                    return DJBHash(bytes);
                case 6:
                    return SDBMHash(bytes);
                case 7:
                    return PJWHash(bytes);
            }
            throw new IllegalArgumentException("Invalid index: " + index);
        }

        static int RSHash(byte[] bytes) {
            int hash = 0;
            int magic = 63689;
            for (byte b : bytes) {
                hash = hash * magic + b;
                magic = magic * 378551;
            }
            return hash;
        }

        static int JSHash(byte[] bytes) {
            int hash = 1315423911;
            for (byte b : bytes) {
                hash ^= ((hash << 5) + b + (hash >> 2));
            }
            return hash;
        }

        static int ELFHash(byte[] bytes) {
            int hash = 0;
            int x;
            for (byte b : bytes) {
                hash = (hash << 4) + b;
                if ((x = hash & 0xF0000000) != 0) {
                    hash ^= (x >> 24);
                    hash &= ~x;
                }
            }
            return hash;
        }

        static int BKDRHash(byte[] bytes) {
            int seed = 131;
            int hash = 0;
            for (byte b : bytes) {
                hash = (hash * seed) + b;
            }
            return hash;
        }

        static int APHash(byte[] bytes) {
            int hash = 0;
            int len = bytes.length;
            for (int i = 0; i < len; i++) {
                if ((i & 1) == 0) {
                    hash ^= ((hash << 7) ^ bytes[i] ^ (hash >> 3));
                } else {
                    hash ^= (~((hash << 11) ^ bytes[i] ^ (hash >> 5)));
                }
            }
            return hash;
        }

        static int DJBHash(byte[] bytes) {
            int hash = 5381;
            for (byte b : bytes) {
                hash = ((hash << 5) + hash) + b;
            }
            return hash;
        }

        static int SDBMHash(byte[] bytes) {
            int hash = 0;
            for (byte b : bytes) {
                hash = b + (hash << 6) + (hash << 16) - hash;
            }
            return hash;
        }

        static int PJWHash(byte[] bytes) {
            long bitsInUnsignedInt = (4 << 3);
            long threeQuarters = ((bitsInUnsignedInt * 3) >> 2);
            long oneEighth = (bitsInUnsignedInt >> 3);
            long highBits = (long) (0xFFFFFFFF) << (bitsInUnsignedInt - oneEighth);
            int hash = 0;
            long test;
            for (byte b : bytes) {
                hash = (hash << oneEighth) + b;
                if ((test = hash & highBits) != 0) {
                    hash = (int) ((hash ^ (test >> threeQuarters)) & (~highBits));
                }
            }
            return hash;
        }
    }
}
复制代码

3:BloomFilter 集群使用

BloomFilter的单机使用的类库已足够的方便,实际开发过程中始终会面临集群的问题。因为rest或rpc 服务基本都是无状态的,不会将某一个请求固定在某台机器上。接下来我们需要引入redis的插件RedisBloom。

1:下载git 仓库,make编译。

wget https://github.com/RedisBloom/RedisBloom/archive/refs/tags/v2.2.5.tar.gz --no-check-certificate

## 下载完毕以后解压,然后make 编译
make 
复制代码

Snip20210712_73.png 当时在mac 环境下编译,执行一直失败。没有找到相关的解决方案,随后我在阿里云上找了一个按量付费的机器。安装完毕以后释放,也就几毛钱。 如下图 ,编译完成以后会生成redisbloom.so。

Snip20210711_67.png

2:centos7.6 安装redis。

详细安装步骤如下:

## 先安排依赖
yum install -y cpp binutils glibc glibc-kernheaders glibc-common glibc-devel gcc make tcl

##centos7 默认的 gcc 版本小于 5.3 无法编译
sudo yum -y install centos-release-scl
sudo yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils

##临时生效,退出 shell 或重启会恢复原 gcc 版本
sudo scl enable devtoolset-9 bash

##永久生效
sudo echo "source /opt/rh/devtoolset-9/enable" >>/etc/profile

## https 下载要上加上  --no-check-certificate
wget https://download.redis.io/releases/redis-6.2.4.tar.gz   --no-check-certificate


## 解压安装
tar -zxvf redis-6.2.4.tar.gz
cd redis-6.2.4
make
make test
make install


创建根目录下的 redis 配置
sudo mkdir /etc/redis
## cp redis-6.2.4 下面的redis conf 到 /etc/redis/
sudo cp redis.conf /etc/redis/

在 /etc/systemd/system新建service文件
sudo vi /etc/systemd/system/redis.service 

[Unit]
Description=Redis
After=network.target

[Service]
#Type=forking
ExecStart=/usr/local/bin/redis-server /etc/redis/redis.conf
ExecReload=/usr/local/bin/redis-server -s reload
ExecStop=/usr/local/bin/redis-server -s stop
PrivateTmp=true

[Install]
WantedBy=multi-user.target

加入开启启动
sudo systemctl daemon-reload
sudo systemctl enable redis


启动服务:
sudo systemctl restart redis
复制代码

3:redis加载redisbloom.so。

注意redisbloom.so 我直接配置在 redis.conf ,这样启动的时候就默认加载。redis的其他配置比如bind 127.0.0.1 , requirepass 123456 其他的根据自己的需求在redis.conf中设置。 Snip20210711_69.png

4:redis bloom 测试,详细如下。

Snip20210711_68.png

5:JRedisBloom 依赖包。

RedisBloom 提供了丰富的client ,详细见:RedisBloom 。 针对于Python、Java、Go、Php、.Net都有实现。 注意:如果你引入的是redis client 是redis.clients,只需引入jrebloom maven即可实现RedisBloom。 如果你使用的是spring-data-redis,请继续往下看。

 <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.6.1</version>
 </dependency>
 <dependency>
      <groupId>com.redislabs</groupId>
      <artifactId>jrebloom</artifactId>
      <version>2.1.0</version>
</dependency>
复制代码

4:Spring Data Redis 如何引入BloomFilter

我们的推荐系统工程是基于的spingboot,redis引入的是spring-data-redis。当时RedisBloom没有spring-data-redis实现,已经准备再通过redis.clients 初始化。后面发现 https://github.com/redooper/spring-data-redis-bloom-filter 基于 RedisTemplate execute 实现。下载到本地打包完成,上传到本地私服即可。 Snip20210712_74.png 注意:源码里面在创建bloomfilter的时候,没有给bloomfilter key过期时间。我们的场景在创建bloomfilter的时候,需默认给一个过期时间(finally加过期处理)。为了避免同一个时间点key大面积失效,在过期时间的基础上加一个随机时间。避免redis同一时间点,失效过多引起不必要的问题。

5:总结

BloomFilter之所以能做到在时间和空间上的效率比较高,是因为牺牲了判断的准确率、删除的便利性。所以我们在使用BloomFilter的场景中,要允许出现一定的准确性。我们在推荐系统多路召回需要把用户最近展示过的内容剔除,所以引入RedisBloom。我们也是边踩坑,边找解决方案。我始终相信,所有的问题都可以解决,无非在业务或技术方案去突破。文中如果有不准确的地方,所以大家一起指正交流。git上也有人提供布谷鸟的概念,可以针对布隆过滤器进行删除,有兴趣的同学可以了解一下。详细见redis-cuckoofilter

分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改