布隆过滤器本质上是一个巨大的二进制数组,以Redis中的布隆过滤器实现为例,Redis中的布隆过滤器底层是一个大型位数组(二进制数组)+多个无偏hash函数。
多个无偏hash函数:
无偏hash函数就是能把元素的hash值计算的比较均匀的hash函数,能使得计算后的元素下标比较均匀的映射到位数组中。
如下就是一个简单的布隆过滤器示意图,其中k1、k2代表增加的元素,a、b、c即为无偏hash函数,最下层则为二进制数组。
注意,这里abc是三个Hash函数,k1和k2是两个要存进来的数据。存进来以后,某几个位置上就被置为1。
在查询的时候,要计算该元素的三个hash函数,然后去找每个位置上的0或者1,如果所有的都是1,就是存在,如果有任何一个位置为0,就不存在。
但是,这也会导致误判,因为某些元素会计算得出相同的位,也就是说在计算中会出现误判——把不存在于数据中的元素当成了存在的。
同样,也不能删除,因为删除会删除掉其他数据。
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());
}
影响误判率的主要因素就是 布隆过滤器数据所占的空间大小,以及哈希方法的个数。
如果需要误判率低,就需要布隆过滤器空间更大,以及哈希方法更多。