Redis布隆过滤器(Bloom Filter)

1. 概念

Bloom Filter可以理解为是一个m位的数组,它有k个相互独立的哈希函数。每当新来一个元素时,会使用这些哈希函数分别计算,将对应位置置为1。查询时也要进行k次哈希运算,如果对应的k个位置都为1,则表明元素可能存在。

Bloom Filter示意图:
假如当前的过滤器中已经记录了1、10,此时判断3是否存在,匹配之后发现并不存在



(图中的方格就代表着数组,每个数字指向二位数组的线就代表一个哈希元素。)

注:布隆过滤器只能判断某个值一定不存在,无法判断某个值一定存在。

2. 具体使用

2.1 google.guava

Google在guava包中提供了一个布隆过滤器的实现——com.google.common.hash.BloomFilter
put(T object) //往布隆过滤器中存入object
mightContain(T object) //判断object是否存在

直接贴代码,首先是pom


    com.google.guava
    guava
    30.1.1-jre

首先在配置类中创建BloomFilter的Bean,方便注入引用

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.nio.charset.Charset;

@Configuration
public class GoogleBloomFilter {

    @Bean
    public BloomFilter initBloomFilterHelper() {
        return BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf-8")), 1000, 0.00001);
    }
}

模拟插入操作,如果存在则不插入。

@RestController
@RequestMapping("/bloom")
@AllArgsConstructor
public class RedisController {

    private final BloomFilter bloomFilter;

    @GetMapping("/string/add")
    public String redisString(String value){
        boolean isExist = bloomFilter.mightContain(value);
        System.out.println(value+ "是否存在:" + isExist);
        if (!isExist) {
            System.out.println("插入:" + value);
            bloomFilter.put(value);
        }
        return String.valueOf(isExist);
    }
}

测试用例:
13333333333
13333333334
13333333335
13333333336
13333333337
13333333335
13333333337

结果:

13333333333是否存在:false
插入:13333333333
13333333334是否存在:false
插入:13333333334
13333333335是否存在:false
插入:13333333335
13333333336是否存在:false
插入:13333333336
13333333337是否存在:false
插入:13333333337
13333333335是否存在:true
13333333337是否存在:true
2.2 google.guava的BloomFilter源码解析

首先看一下BloomFilter的成员变量

/** BloomFilter的位集,也就是上文所说的数组 */
private final LockFreeBitArray bits;

/** 表示哈希函数的个数 */
private final int numHashFunctions;

/** 把任意类型的数据转换为Java基本数据类型,最终转化为byte数组 */
private final Funnel funnel;

/** 内部接口,将元素T映射到numHashFunctions位索引的策略 */
private final Strategy strategy;

然后看一下4个构造方法

public static  BloomFilter create(
    Funnel funnel, int expectedInsertions, double fpp) {
    return create(funnel, (long) expectedInsertions, fpp);
}

public static  BloomFilter create(
    Funnel funnel, long expectedInsertions, double fpp) {
    return create(funnel, expectedInsertions, fpp, BloomFilterStrategies.MURMUR128_MITZ_64);
}

public static  BloomFilter create(Funnel funnel, int expectedInsertions) {
    return create(funnel, (long) expectedInsertions);
}

public static  BloomFilter create(Funnel funnel, long expectedInsertions) {
    return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions
}

四个构造方法没太大的区别,最后都会指向同一个create方法
这个方法中接收了4个参数:funnel(输入的数据),expectedInsertions(预计插入的元素总数),fpp(期望误判率),strategy(实现Strategy的实例,参考上面的公共构造方法,默认传递的是BloomFilterStrategies.MURMUR128_MITZ_64)

@VisibleForTesting
static  BloomFilter create(
    Funnel funnel, long expectedInsertions, double fpp, Strategy strategy) {
    //各种参数校验
    checkNotNull(funnel);
    checkArgument(
        expectedInsertions >= 0, "Expected insertions (%s) must be >= 0", expectedInsertions);
    checkArgument(fpp > 0.0, "False positive probability (%s) must be > 0.0", fpp);
    checkArgument(fpp < 1.0, "False positive probability (%s) must be < 1.0", fpp);
    checkNotNull(strategy);
    
    if (expectedInsertions == 0) {
      expectedInsertions = 1;
    }
    /*
     * TODO(user): Put a warning in the javadoc about tiny fpp values, since the resulting size
     * is proportional to -log(p), but there is not much of a point after all, e.g.
     * optimalM(1000, 0.0000000000000001) = 76680 which is less than 10kb. Who cares!
     */
    //计算数组长度
    long numBits = optimalNumOfBits(expectedInsertions, fpp);
    //计算出每个元素需要哈希方法的个数
    int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
    try {
      //创建BloomFilter对象
      return new BloomFilter(new LockFreeBitArray(numBits), numHashFunctions, funnel, strategy);
    } catch (IllegalArgumentException e) {
      throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", e);
    }
}

最后再看一下BloomFilter这个构造方法,很简单,检查完了之后给4个成员变量赋值

/** Creates a BloomFilter. */
private BloomFilter(
    LockFreeBitArray bits, int numHashFunctions, Funnel funnel, Strategy strategy) {
    checkArgument(numHashFunctions > 0, "numHashFunctions (%s) must be > 0", numHashFunctions);
    checkArgument(
        numHashFunctions <= 255, "numHashFunctions (%s) must be <= 255", numHashFunctions);
    this.bits = checkNotNull(bits);
    this.numHashFunctions = numHashFunctions;
    this.funnel = checkNotNull(funnel);
    this.strategy = checkNotNull(strategy);
}

再看一下mightContain方法和put方法

public boolean mightContain(T object) {
    return strategy.mightContain(object, funnel, numHashFunctions, bits);
}

@CanIgnoreReturnValue
public boolean put(T object) {
    return strategy.put(object, funnel, numHashFunctions, bits);
}

都是调用的成员变量strategy的方法,我们知道构造函数的strategy的参数都是BloomFilterStrategies.MURMUR128_MITZ_64这个枚举值,贴一下代码

MURMUR128_MITZ_64() {
    @Override
    public  boolean put(
        T object, Funnel funnel, int numHashFunctions, LockFreeBitArray bits) {
      long bitSize = bits.bitSize();
      byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
      long hash1 = lowerEight(bytes);
      long hash2 = upperEight(bytes);

      boolean bitsChanged = false;
      long combinedHash = hash1;
      for (int i = 0; i < numHashFunctions; i++) {
        // Make the combined hash positive and indexable
        bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize);
        combinedHash += hash2;
      }
      return bitsChanged;
    }

    @Override
    public  boolean mightContain(
        T object, Funnel funnel, int numHashFunctions, LockFreeBitArray bits) {
      long bitSize = bits.bitSize();
      byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
      long hash1 = lowerEight(bytes);
      long hash2 = upperEight(bytes);

      long combinedHash = hash1;
      for (int i = 0; i < numHashFunctions; i++) {
        // Make the combined hash positive and indexable
        if (!bits.get((combinedHash & Long.MAX_VALUE) % bitSize)) {
          return false;
        }
        combinedHash += hash2;
      }
      return true;
    }

抽象来看,put是写,mightContain是读,两个方法的代码有一点相似,都是先利用murmur3 hash对输入的funnel计算得到128位的字节数组,然后高低分别取8个字节(64位)创建2个long型整数hash1,hash2作为哈希值。循环体内采用了2个函数模拟其他函数的思想,即上文提到的gi(x) = h1(x) + ih2(x) ,这相当于每次累加hash2,然后通过基于bitSize取模的方式在bit数组中索引。

这里之所以要和Long.MAX_VALUE进行按位与的操作,是因为在除数和被除数符号不一致的情况下计算所得的结果是有差别的,在程序语言里,“%”准确来说是取余运算(C,C++和Java均如此,python是取模),如-5%3=-2,而取模的数学定义是x mod y=x-y[x/y](向下取整),所以-5 mod 3= -5-3*(-2)=1,因此当哈希值为负数的时候,其取余的结果为负(bitSize始终为正数),这样就不方便在bit数组中取值,因此通过Long.MAX_VALUE(二进制为0111…1111),直接将开头的符号位去掉,从而转变为正数。当然也可以取绝对值,在另一个MURMUR128_MITZ_32的实现中就是这么做的。

在put方法中,先是将索引位置上的二进制置为1,然后用bitsChanged记录插入结果,如果返回true表明没有重复插入成功,而mightContain方法则是将索引位置上的数值取出,并判断是否为0,只要其中出现一个0,那么立即判断为不存在。

这种使用方式属于单机版的布隆过滤器的使用,在分布式场景下并不试用,所以需要redis来做一个分布式场景下的布隆过滤器

总结:

  1. BloomFilter类就是利用公式完成对参数的估算,创建了一个本地的BitArray数组(一个Long类型的数组,长度m)和需要哈希方法的次数k。
  2. 之后利用MURMUR128_MITZ_64这个枚举值中的方法来进行运算,在put方法中,利用计算出来的k个数值。在BitArray中,以这k个数值为下标,将原有为0的值修改为1。
  3. mightContain方法中,跟put方法同理,计算出k个数值,在BitArray中判断这些数值为下标的值是否为0,只要出现一个0则返回false。
  4. 仅适合一个节点的使用,因为分布式中每个节点的BloomFilter的bean都是独立的。
2.3 利用Redis Bitmaps进行重构

大致思路:创建一个新的BloomFilter,4个成员变量中,将Strategy这个变量改为使用Redis BitMap,其余的3个成员变量及计算方式保留。将MURMUR128_MITZ_64中的Hashing.murmur3_128()方法拿出来,得到计算出来的k个下标,之后循环这些下标将BitMap中的这些值修改为1。

了解下Redis Bitmaps的结构,推荐一篇不错的博客:https://www.cnblogs.com/54chensongxia/p/13794391.html

下面贴代码。
首先创建一个BloomFilter类

import com.google.common.base.Preconditions;
import com.google.common.hash.Funnel;
import com.google.common.hash.Hashing;

public class BloomFilterHelper {

    private int numHashFunctions;

    private int bitSize;

    private Funnel funnel;

    public BloomFilterHelper(Funnel funnel, int expectedInsertions, double fpp) {
        Preconditions.checkArgument(funnel != null, "funnel不能为空");
        this.funnel = funnel;
        // 计算bit数组长度
        bitSize = optimalNumOfBits(expectedInsertions, fpp);
        // 计算hash方法执行次数
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
    }

    public 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;
    }

    /**
     * 计算bit数组长度
     * @param n 预计插入的元素总数
     * @param p 期望误判率
     * @return
     */
    private int optimalNumOfBits(long n, double p) {
        if (p == 0) {
            // 设定最小期望长度
            p = Double.MIN_VALUE;
        }
        int sizeOfBitArray = (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
        return sizeOfBitArray;
    }

    /**
     * 计算hash方法执行次数
     * @param n 预计插入的元素总数
     * @param m bit数组长度
     * @return
     */
    private int optimalNumOfHashFunctions(long n, long m) {
        int countOfHash = Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
        return countOfHash;
    }
}

接下来就是初始化一个上面自己实现的布隆过滤器类的bean

import com.google.common.base.Charsets;
import com.google.common.hash.Funnel;
import com.yyhd.redisdemo.filter.BloomFilterHelper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GoogleBloomFilter {

    @Bean
    public BloomFilterHelper initBloomFilterHelper() {
        BloomFilterHelper bloomFilter = new BloomFilterHelper((Funnel) (from, into) -> {
            into.putString(from, Charsets.UTF_8);
        }, 1000000, 0.0000001);
        return bloomFilter;
    }
}

之后创建BloomFilterUtil(主要操作的类),利用BloomFilterHelper的murmurHashOffset计算出哈希方法的数组,循环数组以每个值为下标,将Redis BitMap中这些值设置为1。(我用的是jedis,具体方法不贴了网上有很多)

import com.yyhd.redisdemo.filter.BloomFilterHelper;
import com.yyhd.redisdemo.util.RedisUtil;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@AllArgsConstructor
public class BloomFilterUtil {

    private final BloomFilterHelper bloomFilterHelper;
    private final RedisUtil redisUtil;

    public  void addBloomFilter(String key, T value) {
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
            redisUtil.setbit(key, i, true);
        }
    }

    public  boolean includeByBloomFilter(String key, T value) {
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
            if (!redisUtil.getbit(key, i)) {
                return false;
            }
        }
        return true;
    }
}

最后进行测试:

@RestController
@RequestMapping("/bloom")
@AllArgsConstructor
public class RedisController {

    private final BloomFilterUtil bloomFilterUtil;

    @GetMapping("/string/add")
    public String redisString(String value){
        boolean isExist = bloomFilterUtil.includeByBloomFilter("mobile", value);
        System.out.println(value+ "是否存在:" + isExist);
        if (!isExist) {
            System.out.println("插入:" + value);
            bloomFilterUtil.addBloomFilter("mobile", value);
        }

        return String.valueOf(isExist);
    }
}

测试用例:
13333333333
13333333334
13333333335
13333333336
13333333337
13333333335
13333333337

输出结果:

13333333333是否存在:false
插入:13333333333
13333333334是否存在:false
插入:13333333334
13333333335是否存在:false
插入:13333333335
13333333336是否存在:false
插入:13333333336
13333333337是否存在:false
插入:13333333337
13333333335是否存在:true
13333333337是否存在:true

你可能感兴趣的:(Redis布隆过滤器(Bloom Filter))