当我们使用主流数据结构如 Lists, Maps, Sets, Trees等等时,我可以得到确切的结果,无论这个数据存在或是不存在。概率数据结构能够提供一种基于内存的,快速的查找出一种可能而非确切的结果。这种概率数据结构就是布隆过滤器(Bloom filter)。 布隆过滤器可以检查值是“可能在集合中”还是“绝对不在集合中”。
2.1 定义布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
•二进制向量,简单理解就是一个二进制数组。这个数组里面存放的值要么是0,要么是1。•映射函数,它可以将一个元素映射成一个位阵列(Bit array)中的一个点。所以通过这个点,就能判断集合中是否有此元素。
2.2 基本思想•当一个元素被加入集合时,通过K个散列函数将这个元素映射到一个位数组中的K个点,把它们置为1。•检索某个元素时,再通过这K个散列函数将这个元素映射,看看这些位置是不是都是1就能知道集合中这个元素存不存在。如果这些位置有任何一个0,则该元素一定不存在;如果都是1,则被检元素很可能存在。•Bloom Filter跟单个哈希函数映射不同,Bloom Filter使用了k个哈希函数,每个元素跟k个bit对应。从而降低了冲突的概率。
2.3 特性
1)一个元素如果判断结果为存在的时候元素不一定存在,但是判断结果为不存在的时候则一定不存在。也就是说布隆过滤器只能判断数据是否一定不存在,而无法判断数据是否一定存在。 2) 布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。
2.4 优缺点 2.4.1 优点1.二进制组成的数组,内存占用空间少,并且插入和查询速度很快,常数级别。2.Hash函数相互之间没有必然联系,方便由硬件并行实现。3.只存储0和1,不需要存储元素本身,在某些对保密要求非常严格的场合有优势。
2.4.2 缺点1.
存在误差率。随着存入的元素数量增加,误算率随之增加。(比如现实中你是否遇到正常邮件也被放入垃圾邮件目录,正常短信被拦截)可以增加一个小的白名单,存储那些可能被误判的元素。
2.
删除困难。一个元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其他元素的判断。因为其他元素的映射也有可能在相同的位置置为1。可以采用Counting Bloom Filter解决。
二、实践方案 2.1 Guava 实现 BloomFilter首先引入guava的jar包,
com.google.guava guava27.0.1-jre
在Java代码中做如下的创建
public static void main(String[] args) {
// 1.创建符合条件的布隆过滤器
// 预期数据量10000,错误率0.0001
BloomFilter bloomFilter =
BloomFilter.create(Funnels.stringFunnel(
Charset.forName("utf-8")),10000, 0.0001);
// 2.将一部分数据添加进去
for (int i = 0; i < 5000; i++) {
bloomFilter.put("" + i);
}
System.out.println("数据写入完毕");
// 3.测试结果
for (int i = 0; i < 10000; i++) {
if (bloomFilter.mightContain("" + i)) {
System.out.println(i + "存在");
} else {
System.out.println(i + "不存在");
}
}
}
2.2 Redission 实现方案
public class RedissonBloomFilterDemo {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
RBloomFilter bloomFilter = redisson.getBloomFilter("user");
// 初始化布隆过滤器,预计统计元素数量为55000000,期望误差率为0.03
bloomFilter.tryInit(55000000L, 0.03);
bloomFilter.add("Tom");
bloomFilter.add("Jack");
System.out.println(bloomFilter.count()); //2
System.out.println(bloomFilter.contains("Tom")); //true
System.out.println(bloomFilter.contains("Linda")); //false
}
}
三、实际应用场景
以邮箱黑名单为例,当用户注册或者登录请求的时候,我们需要判断用户是否属于这10亿个黑名单邮箱中,如果存在则需要对该用户进行拦截。业务规则虽然简单,难点在于数据量巨大,如何存储这10亿个元素,并且要能快速在10个元素之间去检索
import com.google.common.base.Preconditions; import com.google.common.hash.Funnel; import com.google.common.hash.Hashing; import org.springframework.data.redis.core.RedisTemplate; public class RedisBloom{ private int numHashFunctions; private int bitSize; private Funnel funnel; private RedisTemplate redisTemplate; private RedisBloom(){ } public RedisBloom(Funnel funnel, int expectedInsertions, double fpp, RedisTemplate redisTemplate) { Preconditions.checkArgument(funnel != null, "funnel不能为空"); this.funnel = funnel; bitSize = optimalNumOfBits(expectedInsertions, fpp); numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize); this.redisTemplate = redisTemplate; } private int[] murmurHashOffset(T value) { int[] offset = new int[numHashFunctions]; long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong(); int hash1 = (int) hash64; int hash2 = (int) (hash64 >>> 32); for (int i = 1; i <= numHashFunctions; i++) { int nextHash = hash1 + i * hash2; if (nextHash < 0) { nextHash = ~nextHash; } offset[i - 1] = nextHash % bitSize; } return offset; } private int optimalNumOfBits(long n, double p) { if (p == 0) { p = Double.MIN_VALUE; } return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2))); } private int optimalNumOfHashFunctions(long n, long m) { return Math.max(1, (int) Math.round((double) m / n * Math.log(2))); } public void addByBloomFilter(String key, T value) { int[] offset = this.murmurHashOffset(value); for (int i : offset) { redisTemplate.opsForValue().setBit(key, i, true); } } public boolean includeByBloomFilter(String key, T value) { int[] offset = murmurHashOffset(value); for (int i : offset) { if (!redisTemplate.opsForValue().getBit(key, i)) { return false; } } return true; } }
RedisBloom是实现功能的关键,包含了计算bitmap的核心算法,其实大部分代码都是来源于Guava库里面的BloomFilterStrategies类,但是因为这个类是专门为Guava的BloomFilter类使用的,所以没有对外暴露一些重要的算法逻辑。再来看怎么结合redis一起使用BloomFilter,addByBloomFilter,往redis里面添加元素includeByBloomFilter,检查元素是否在redis bloomFilter里面。



