布隆过滤器是什么
布隆过滤器底层基于bitmap,根据hash函数结果维护。
优点
- 布隆过滤器解决大数据量的”是否存在“问题很好用,能节省大量空间。
缺点
- 布隆过滤器存在一定的误判率,只能确认数据一定不存在,无法确认数据是否一定存在。对于要求精确的数据不适用。
- 布隆过滤器不支持扩展。随着数据量的上升,误判率会跟着上升。此时如果要扩展布隆过滤器,只能新建一个新的布隆过滤器。
基于redis实现布隆过滤器
redis提供setbit等命令,允许对bitmap进行位级别的操作。借助redis提供的接口,不需要自己去实现布隆过滤器,但在需要频繁和布隆过滤器交互的场景不适合使用。
比如在匹配算法中,要求匹配过的人不能重复匹配到一起。在一个时间复杂度为n^2的匹配过程中,如果每次判断都去访问redis的布隆过滤器,可能会导致千万级别的网络交互,大大降低性能。因此实现一套自定义的布隆过滤器显得尤其重要。只需要在匹配开始前,从redis获取布隆过滤器到jvm内存,匹配过程一边判断一边更新布隆过滤器,结束的时候将布隆过滤器更新到redis即可。这样,一次匹配过程只需要和redis交互两次。
自定义布隆过滤器
public class CustomizeBloom {
//预计插入量
private long expectedInsertions = 1000000;
//可接受的错误率
private double fpp = 0.01;
//bit数组长度
private int numBits;
//hash函数数量
private int numHashFunctions;
private long[] bitMap;
private int bitMapLength;
public static void main(String[] args) {
CustomizeBloom customizeBloomJob = new CustomizeBloom();
customizeBloomJob.initParams();
int exitsNums = 0;
for (int i = 0; i < 800000; i++) {
customizeBloomJob.put(String.valueOf(i));
}
for (int i = 0; i <1000000; i++) {
if (customizeBloomJob.isExist(String.valueOf(i))) {
exitsNums++;
}
}
System.out.println("exitsNums = " + exitsNums);
}
public void initParams() {
this.numBits = (int)optimalNumOfBits(expectedInsertions, fpp);
this.numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
initBitMap();
}
private void initBitMap() {
long bitMapLength = this.numBits / 64;
if ((this.numBits & 63) != 0) {
bitMapLength++;
}
if (bitMapLength > Integer.MAX_VALUE) {
throw new RuntimeException("数组长度太长");
}
this.bitMap = new long[(int)bitMapLength];
this.bitMapLength = (int)bitMapLength;
}
//计算hash函数个数
private int optimalNumOfHashFunctions(long n, long m) {
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
//计算bit数组长度
private long optimalNumOfBits(long n, double p) {
if (p == 0) {
p = Double.MIN_VALUE;
}
return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
/**
* 判断keys是否存在于集合
*/
public boolean isExist(String key) {
int[] indexs = getIndexs(key);
for (int index : indexs) {
int bitMapIndex = index / 64;
int bitIndex = index & 63;
long l = (long)1 << bitIndex;
long result = this.bitMap[bitMapIndex] & l;
if (result == 0) {
return false;
}
}
return true;
}
/**
* 将key存入redis bitmap
*/
public void put(String key) {
int[] indexs = getIndexs(key);
for (int index : indexs) {
int bitMapIndex = index / 64;
int bitIndex = index & 63;
long l = this.bitMap[bitMapIndex];
long i = (long)1 << bitIndex;
long result = l | i;
this.bitMap[bitMapIndex] = result;
}
}
/**
* 根据key获取bitmap下标数组
*/
private int[] getIndexs(String key) {
int[] indexs = new int[numHashFunctions];
// 不允许为负数
for (int i = 0; i < numHashFunctions; i++) {
int index = hash1(key) + i*hash2(key);
if (index < 0) {
index = ~index;
}
indexs[i] = index % numBits;
}
return indexs;
}
public int hash1(String key) {
int h;
return (h = key.hashCode()) ^ (h >>> 16);
}
public int hash2(String key) {
int h;
return h = key.hashCode();
}
}
优化方向
- 封装成一个模块,适用于任何需要判断”是否存在“的功能
- 设置bitmap的长度为2^n次方,方便做取余运算
- 根据预插入量和容错率,支持动态扩展。如果bitmap过大,无法依靠一维数组维护,可扩展为二维数组维护
- 增加对边界值的判断,增强代码的健壮性