硬核!从数学原理开始解析布隆过滤器,并解决redis缓存穿透

关于这方面的内容,已经有很多大佬发表过很多博客了,这篇文章更多的只是记录,自己觉得有趣的且有用的一些内容供自己阅读,如果你也喜欢,不慎荣幸!

布隆过滤器是什么

问题引入

问:如何在海量元素中(例如 10 亿无序、不定长、不重复)快速判断一个元素是否存在?

好,我们最简单的想法就是把这么多数据放到数据结构里去,比如List、Map、Tree,一搜不就出来了吗,比如map.get(),我们假设一个元素1个字节的字段,10亿的数据大概需要 900G 的内存空间,这个对于普通的服务器来说是承受不了的,当然面试官也不希望听到你这个答案,因为太笨了吧,我们肯定是要用一种好的方法,巧妙的方法来解决,这里引入一种节省空间的数据结构,位图,他是一个有序的数组,只有两个值,0 和 1。0代表不存在,1代表存在。
位图

那么有了位图来表示,是否存在某个数据,还需要什么呢?

每错,映射关系!用来将数据映射到位图上。映射关系就是我们所说的哈希函数,每一个数据使用哈希函数都会得到一个唯一结果。用哈希函数有两个好处,第一是哈希函数无论输入值的长度是多少,得到的输出值长度是固定的,第二是他的分布是均匀的,如果全挤的一块去那还怎么区分,比如MD5、SHA-1这些就是常见的哈希算法。

问题又来了,这不就是HashMap吗?那我不直接用 hashMap 不就得了?搞这个布隆过滤器干嘛?

在这里插入图片描述

我们通过哈希函数计算以后就可以到相应的位置去找是否存在了,我们看红色的线,24和147经过哈希函数得到的哈希值是一样的,我们把这种情况叫做哈希冲突或者哈希碰撞。要注意,我们的数据量是数十亿啊!这hash冲突直接上天了都。

那怎么办?

哈希碰撞是不可避免的,我们能做的就是降低哈希碰撞的概率,第一种是可以扩大维数组的长度或者说位图容量,因为我们的函数是分布均匀的,所以位图容量越大,在同一个位置发生哈希碰撞的概率就越小。但是越大的位图容量,意味着越多的内存消耗,所以我们想想能不能通过其他的方式来解决。
第二种方式就是经过多几个哈希函数的计算,你想啊,24和147现在经过一次计算就碰撞了,那我经过5次,10次,100次计算还能碰撞的话那真的是缘分了。但也不是越多次哈希函数计算越好,因为这样很快就会填满位图,而且计算也是需要消耗时间,所以我们需要在时间和空间上寻求一个平衡。而这就是我们的布隆过滤器了。

布隆过滤器

早在 1970 年的时候,有一个叫做布隆的前辈对于判断海量元素中元素是否存在的问题进行了研究,也就是到底需要多大的位图容量和多少个哈希函数,它发表了一篇论文,提出的这个容器就叫做布隆过滤器。

布隆过滤器原理
大家来看下这个图,我们看集合里面3个元素,现在我们要存了,比如说a,经过f1(a),f2(a),f3(a)经过三个哈希函数的计算,在相应的位置上存入1,元素b,c也是通过这三个函数计算放入相应的位置。当取的时候,元素a通过f1(a)函数计算,发现这个位置上是1,没问题,第二个位置也是1,第三个位置上也是 1,这时候我们说这个a在布隆过滤器中是存在的,没毛病,同理我们看下面的这个d,通过三次计算发现得到的结果也都是1,那么我们能说d在布隆过滤器中是存在的吗,显然是不行的,我们仔细看d得到的三个1其实是f1(a),f1(b),f2©存进去的,并不是d自己存进去的,这个还是哈希碰撞导致的,我们把这种本来不存在布隆过滤器中的元素误判为存在的情况叫做假阳性(False Positive Probability,FPP)。

我们再来看另一个元素,e 元素。我们要判断它在容器里面是否存在,一样地要用这三个函数去计算。第一个位置是 1,第二个位置是 1,第三个位置是 0。那么e元素能不能判断是否在布隆过滤器中?答案是肯定的,e一定不存在。你想啊,如果e存在的话,他存进去的时候这三个位置都置为1,现在查出来有一个位置是0,证明他没存进去啊。。通过上面这张图加说明,我们得出两个重要的结论

从容器的角度来说:

  • 如果布隆过滤器判断元素在集合中存在,不一定存在
  • 如果布隆过滤器判断不存在,一定不存在
  • 从元素的角度来说:
  • 如果元素实际存在,布隆过滤器一定判断存在
  • 如果元素实际不存在,布隆过滤器可能判断存在

Guava 实现布隆过滤器

导入jar包

<dependency> 
    <groupId>com.google.guava</groupId> 
    <artifactId>guava</artifactId> 
    <version>21.0</version> 
</dependency>

实现

//插入多少数据 
   private static final int insertions = 1000000; 
 
   //期望的误判率 
   private static double fpp = 0.02; 
 
   public static void main(String[] args) { 
 
       //初始化一个存储string数据的布隆过滤器,默认误判率是0.03 
       BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), insertions, fpp); 
 
       //用于存放所有实际存在的key,用于是否存在 
       Set<String> sets = new HashSet<String>(insertions); 
 
       //用于存放所有实际存在的key,用于取出 
       List<String> lists = new ArrayList<String>(insertions); 
 
       //插入随机字符串 
       for (int i = 0; i < insertions; i++) { 
           String uuid = UUID.randomUUID().toString(); 
           bf.put(uuid); 
           sets.add(uuid); 
           lists.add(uuid); 
       } 
 
       int rightNum = 0; 
       int wrongNum = 0; 
 
       for (int i = 0; i < 10000; i++) { 
           // 0-10000之间,可以被100整除的数有100个(100的倍数) 
           String data = i % 100 == 0 ? lists.get(i / 100) : UUID.randomUUID().toString(); 
 
           //这里用了might,看上去不是很自信,所以如果布隆过滤器判断存在了,我们还要去sets中实锤 
           if (bf.mightContain(data)) { 
               if (sets.contains(data)) { 
                   rightNum++; 
                   continue; 
               } 
               wrongNum++; 
           } 
       } 
 
       BigDecimal percent = new BigDecimal(wrongNum).divide(new BigDecimal(9900), 2, RoundingMode.HALF_UP); 
       BigDecimal bingo = new BigDecimal(9900 - wrongNum).divide(new BigDecimal(9900), 2, RoundingMode.HALF_UP); 
       System.out.println("在100W个元素中,判断100个实际存在的元素,布隆过滤器认为存在的:" + rightNum); 
       System.out.println("在100W个元素中,判断9900个实际不存在的元素,误认为存在的:" + wrongNum + ",命中率:" + bingo + ",误判率:" + percent); 
   }

运行结果
在这里插入图片描述
我们看到这个结果正是印证了上面的结论,这100个真实存在元素在布隆过滤器中一定存在,另外9900个不存在的元素,布隆过滤器还是判断了216个存在,这个就是误判,原因上面也说过了,所以布隆过滤器不是万能的,但是它能帮我们抵挡掉大部分不存在的数据已经很不错了,已经减轻数据库很多压力了,另外误判率0.02是在初始化布隆过滤器的时候我们自己设的,如果不设默认是0.03,我们自己设的时候千万不能设0!

Guava 实现布隆过滤器的源码解析

算法流程

BloomFilter的整体算法流程可总结为如下步骤:

  1. BloomFilter初始化为m位长度的位向量,每一位均初始化为0
    在这里插入图片描述

  2. 使用k个相互独立的Hash函数,每个Hash函数将元素映射到{1…m}的范围内,并将对应的位置为1。
    在这里插入图片描述

  3. 若检查一个元素y是否存在,首先第一步使用k个Hash函数将元素y映射到k位。分别检测每一位是否为0。若某一位为0,则元素y一定不存在,若全部为1,则有可能存在。

空间复杂度

BloomFilter 使用位向量来表示元素,而不存储本身,这样极大压缩了元素的存储空间。其空间复杂度为O(m),m是位向量的长度。而m与插入总数量n的关系如下公式(1)所示
m = − n ln ⁡ p ( ln ⁡ 2 ) 2 m = -\dfrac{n \ln p}{(\ln2)^2} m=(ln2)2nlnp

我们可以利用这个公式来算一下需要抓取100万个URL时BloomFilter所占据的空间。

假设要求误判率为1%,因此该公式可转化为
m = 9.6 ∗ n m = 9.6*n m=9.6n
。故此时BloomFilter位向量的大小为 100W*9.6 = 960 Wbit,约1.1M内存空间。
只需要1.1M的内存空间,就可满足100万个url的去重需求,这个空间复杂度之低不可谓不惊人。 实际上,哪怕是1亿个URL,也仅需100M左右的内存空间即可满足BloomFilter的空间需求,这对于绝大部分爬虫的体量来说,是完全可行的。

时间复杂度

时间复杂度方面 BloomFilter的时间复杂度仅与Hash函数的个数k有关,即O(k)

误判率

为什么说,在查找元素时,即使某个元素所映射的k位全部位1,依然无法确定它一定存在?

这是因为当插入的元素很多的情况下,某个元素即使之前不存在,但是它所映射的k位已经被之前其他的元素置为1了,这样就会出现误判,BloomFilter会认为它已经存在了。

但是这个概率是非常小的。根据上面公司的推导公式来说,误判率的大小p满足以下公式(2):

ln ⁡ p = − m n ( ln ⁡ 2 ) 2 \ln p = -\dfrac{m}{n}{(\ln2)^2} lnp=nm(ln2)2

其中m为位向量的长度,n为要插入元素的总数。当误判率为1%时, m/n = 9.6
即每个元素仅需要9.6个字节(bit)存储即可.

Hash函数的个数k,与误判率大小p的关系为如下公式(3) 所示
k = − ln ⁡ p ln ⁡ 2 k = -\dfrac{\ln p}{\ln 2} k=ln2lnp

当误判率大小为0.1时,k为3。当误判率大小为0.01时,k为7

而,k与位向量的长度m和插入元素的总数n的关系为公式(4) 所示

k = m n ln ⁡ 2 k = \dfrac{m}{n}\ln 2 k=nmln2

缺点

删除元素
BloomFilter 由于并不存储元素,而是用位的01来表示元素是否存在,并且很有可能一个位时被多个元素同时使用。所以无法通过将某元素对应的位置为0来删除元素。

当然这肯定是有办法解决的

Counting BloomFilter
布隆过滤器无法支持元素的删除操作,Counting BloomFilter通过存储位元素每一位的置为1的数量,使得布隆过滤器可以支持删除操作。 但是这样会数倍地增加布隆过滤器的存储空间。

Guava’s BloomFilter源码剖析

使用介绍

在这个例子中,我们定义了一个Person类,来表示一个人的姓名信息。代码如下

class Person{
  private String firstName;
  private String lastName;
  //getter.. 
  //setter..
}

在这个例子中,我们会将Person对象传递给BloomFilter,当该对象的firstName+lastName在BloomFilter中存在时,会提示已存在,不将其加入BloomFilter中。

但是对于BloomFilter来说,它无法主动地知道如何把一个自定义类的对象转化为hash值,也许有人会说,重写类的hashcode方法不就行了吗?

事实上还真不行,因为这和BloomFilter的概念冲突了,BloomFilter简单来说是通过计算出k个不同的hash值来定位元素,重写了hashcode那么这个计算k个hash的过程将毫无意义。

Guava通过将自定义对象分解为普通类型,对普通类型进行Hash值的计算,来将对象存入BloomFilter中。

这里Guava引入了一个叫做Funnel的类,Funnel类定义了如何把一个具体的对象类型分解为原生字段值,从而将值分解为Byte以供后面BloomFilter进行hash运算。通过使用这个类,我们可以自己定义一个属于自己类的Funnel。如下代码

enum PersonFunnel implements Funnel<Person> {
    INSTANCE;

    @Override
    public void funnel(Person person, PrimitiveSink into) {
        into.putString(person.getFirstName(), Charset.defaultCharset())
                .putString(person.getLastName(), Charset.defaultCharset());
    }
}

此外,Guava预定义了一些原生类型的Funnel,如String、Long、Integer。具体代码可以在这里看到。

当我们的BloomFilter存储的是这些原生类型时,不用再额外自行写Funnel,直接使用Guava预定义的这些即可。

以下是整个代码调用的流程

class BloomFilterSample{
  public static void main(String[] args) {
        // 创建一个BloomFilter,其预计插入的个数为10,误判率大约为0.01
        BloomFilter<Person> bloomFilter = BloomFilter.create(PersonFunnel.INSTANCE, 10, 0.01);
        // 查询new Person("chen", "yahui")是否存在
        System.out.println(bloomFilter.mightContain(new Person("chen", "yahui"))); //false
        // 将new Person("chen", "yahui")对象放入BloomFilter中
        bloomFilter.put(new Person("chen","yahui"));
        // 再次查询new Person("chen", "yahui")是否存在
        System.out.println(bloomFilter.mightContain(new Person("chen", "yahui"))); //true
  }
}

源码解读

初始化BloomFilter

在例子中,我们通过调用BloomFilter.create工厂方法来生成一个BloomFilter

<T> BloomFilter<T> create(Funnel<? super T> funnel, 
                          long expectedInsertions,  //预期会插入多少元素
                          double fpp, //自定义误判率
                          Strategy strategy //Hash策略 
                          ) {
    if (expectedInsertions == 0) {
      expectedInsertions = 1;
    }
    //根据插入的数量和误判率来得出位向量应有的长度,这里使用的算法就是公式 2
    long numBits = optimalNumOfBits(expectedInsertions, fpp);
    //根据插入的数量和位向量的长度来得出应该用多少个Hash函数,这里使用的算法是公式 4
    int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
    return new BloomFilter<T>(new BitArray(numBits), numHashFunctions, funnel, strategy);
}

整个创建流程非常清晰,如果看懂了本文的算法描述部分,应该不难理解该段代码。

整个初始化流程的目的主要有两个:根据参数计算出位向量的长度以及Hash函数的个数

  1. 根据预期插入的数量expectedInsertions和自定义的误判率fpp来得到位向量的长度numBits,其中optimalNumOfBits的实现如下
// 根据插入的数量和误判率来得出位向量应有的长度
static 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)));
}

可以很明显看出,optimalNumOfBits的源码,其实就是对公式(1)的实现

  1. 根据插入的数量expectedInsertions和位向量的长度numBits来得出应该用多少个Hash函数,其中optimalNumOfHashFunctions的实现如下
static int optimalNumOfHashFunctions(long n, long m) {
    // (m / n) * log(2), but avoid truncation due to division!
    return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
  }

同样,optimalNumOfHashFunctions也对应了我们算法中公式(4)

  1. 根据numBits生成BitArray。 BitArray是Guava中位向量的表示,具体的实现细节,将会在下一节中讲述。

位向量的表示

本节主要讲述了Guava中位向量的表示,此部分对于BloomFilter的整体算法流程的理解关联性并不是特别强。读者如对此部分不感兴趣,可直接跳过此节。

位向量是BloomFilter的存储表示,所有的数据都在BloomFilter中存储。

如何更高效地表示位向量也是一个优质的BloomFilter代码重要考量标准。 在Guava中,自定义了BitArray类,代码如下:

class BitArray {
    final long[] data;
    long bitCount;

    BitArray(long bits) {
      //对于长度为m的位向量来说,对应的long数组的长度应为m/64向上取整。
      this(new long[Ints.checkedCast(LongMath.divide(bits, 64, RoundingMode.CEILING))]);
    }

    boolean set(long index) {
        if (!get(index)) {
          data[(int) (index >>> 6)] |= (1L << index);
          bitCount++;
          return true;
        }
        return false;
    }

    boolean get(long index) {
      return (data[(int) (index >>> 6)] & (1L << index)) != 0;
    }
  }

在BitArray中,使用long数组来表示位向量,一个数组元素对应位向量的64位,所以对于长度为m的位向量来说,对应的long数组的长度应为m/64向上取整。 即

new long[Ints.checkedCast(LongMath.divide(bits, 64, RoundingMode.CEILING))]

在BloomFilter算法讲解部分,我们可以看到,对于位向量的常用操作主要有两个,将位向量某一位置为1以及查看位向量某一位是否为1。分别对应源码中的set操作和get操作。本文只讲下get方法的源码部分,set方法与get方法类似,不再累述。

get方法大致可以分为两部分

  1. data[(int) (index >>> 6)] 定位到元素

上面讲到long数组的每一个元素都包含位向量其中的64位,如果想要找出某个位的bit,那么首先第一步就是定位到该bit所在的元素编号。

我们一般的做法是index/64。 而源码中使用了index >>> 6,逻辑右移6位,
,其效果与除以64相同。采用位运算的速度比普通的除法要快很多。

  1. … & (1L << index) 获取位的状态

源码中直接将要查看的bit以及同一数组元素块的64位bits一起取出,将1L左移index位后求且运算,最终即可得出该位的值。

k个hash函数的选取

在Hash函数的选取方面,一个很重要的问题就是如何选取多个Hash函数?

我们在对元素进行映射的时候,Hash函数的个数k的选取,最少也得为3个,最多可能多达十几个甚至更多,而目前世界上可商用的高质量的开源的Hash方法,远远达不到BloomFilter的需求。 在论文《Bloom Filters in Probabilistic Verification》中提出了一种算法,把原本需要几十个hash函数的bloom filter转化成了两个hash值的运算,完美地解决了这个问题。如下公式(5)所示:
G i ( x ) = h 1 ( x ) + i h 2 ( x ) + i 2 G_i(x) = h_1(x) + ih_2(x) + i^2 Gi(x)=h1(x)+ih2(x)+i2

上述公式中
为第i个hash函数。其中0 < i < k。也就是说使用hash1()和hash2()对一次输入求出两个不同的hash值,然后将这两个hash值代入公式,求出k个hash值。 整体的计算流程如下:

//准备阶段
h1 = hash1(input), h2 = hash2(input)
// 求出k个hash值
g0(x) = h1 第0个hash函数求出的hash值
g1(x) = h1+h2+1 第1个hash函数求出的hash值
g2(x) = h1+2*h2+4 第2个hash函数求出的hash值
gk-1(x) = h1+(k-1)*h2+(k-1)^2 第k-1个hash函数求出的hash值

在哈佛大学2006年的一篇论文《Less hashing, same performance: building a better bloom filter》中,对此方法的有效性进行了验证,证明了此种方法不会对BloomFilter的效率有所恶化。

Guava实现了两种策略MURMUR128_MITZ_32和MURMUR128_MITZ_64,其所有的实现类为BloomFilterStrategies。 本文针对Guava的BloomFilter所采用的MURMUR128_MITZ_32策略进行讲解,

public <T> boolean put(T object, 
                        Funnel<? super T> funnel, 
                        int numHashFunctions, 
                        BitArray bits) {
      long bitSize = bits.bitSize();
      long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong();
      int hash1 = (int) hash64;
      int hash2 = (int) (hash64 >>> 32);

      boolean bitsChanged = false;
      for (int i = 1; i <= numHashFunctions; i++) {
        int combinedHash = hash1 + (i * hash2);
        // Flip all the bits if it's negative (guaranteed positive number)
        if (combinedHash < 0) {
          combinedHash = ~combinedHash;
        }
        bitsChanged |= bits.set(combinedHash % bitSize);
      }
      return bitsChanged;
    }
}

MURMUR128_MITZ_32是对公式(5)的一个实现的体现。

首先根据MurMurHash计算出某个对象的64位hash值,将其分为两段,后32位为hash1,前32位为hash2。 将hash1看成公式(5)中的h1(x)计算后的结果,hash2看做是h2(x)计算后的结果,同时省去公式中的
,将hash1和hash2代入公式计算。

在Hash函数选取方面,Guava采用了MurmurHash3算法。MurmurHash算法是2008年提出的一种Hash算法,运算简单高效,而且随机性强。目前最新的版本为MurmurHash3。

MurmurHash3能够产生出32-bit或128-bit两种哈希值。在MURMUR128_MITZ_32和MURMUR128_MITZ_64都选择使用了128-bit的结果。二者不同的是,MURMUR128_MITZ_32仅使用128-bit的前64位。而MURMUR128_MITZ_64完全使用了128位的结果。

Redis实现布隆过滤器

上面使用guava实现布隆过滤器是把数据放在本地内存中,我们项目往往是分布式的,我们还可以把数据放在redis中,用redis来实现布隆过滤器。

/** 
 * 布隆过滤器核心类 
 * 
 * @param <T> 
 * @author jack xu 
 */ 
public class BloomFilterHelper<T> { 
    private int numHashFunctions; 
    private int bitSize; 
    private Funnel<T> funnel; 
 
    public BloomFilterHelper(int expectedInsertions) { 
        this.funnel = (Funnel<T>) Funnels.stringFunnel(Charset.defaultCharset()); 
        bitSize = optimalNumOfBits(expectedInsertions, 0.03); 
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize); 
    } 
 
    public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) { 
        this.funnel = funnel; 
        bitSize = optimalNumOfBits(expectedInsertions, fpp); 
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize); 
    } 
 	/**
     * 返回key值经过所有哈希函数映射的数值
     */
    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数组长度 
     */ 
    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))); 
    } 
 
    /** 
     * 计算hash方法执行次数 
     */ 
    private int optimalNumOfHashFunctions(long n, long m) { 
        return Math.max(1, (int) Math.round((double) m / n * Math.log(2))); 
    } 
}

这里在操作redis的位图bitmap,你可能只知道redis五种数据类型,string,list,hash,set,zset,没听过bitmap,但是不要紧,你可以说他是一种新的数据类型,也可以说不是,因为他的本质还是string。

/** 
 * redis操作布隆过滤器 
 * 
 * @param <T> 
 * @author xhj 
 */ 
public class RedisBloomFilter<T> { 
    @Autowired 
    private RedisTemplate redisTemplate; 
 
    /** 
     * 删除缓存的KEY 
     * 
     * @param key KEY 
     */ 
    public void delete(String key) { 
        redisTemplate.delete(key); 
    } 
 
    /** 
     * 根据给定的布隆过滤器添加值,在添加一个元素的时候使用,批量添加的性能差 
     * 
     * @param bloomFilterHelper 布隆过滤器对象 
     * @param key               KEY 
     * @param value             值 
     * @param <T>               泛型,可以传入任何类型的value 
     */ 
    public <T> void add(BloomFilterHelper<T> bloomFilterHelper, String key, T value) { 
        int[] offset = bloomFilterHelper.murmurHashOffset(value); 
        for (int i : offset) { 
            redisTemplate.opsForValue().setBit(key, i, true); 
        } 
    } 
 
    /** 
     * 根据给定的布隆过滤器添加值,在添加一批元素的时候使用,批量添加的性能好,使用pipeline方式(如果是集群下,请使用优化后RedisPipeline的操作) 
     * 
     * @param bloomFilterHelper 布隆过滤器对象 
     * @param key               KEY 
     * @param valueList         值,列表 
     * @param <T>               泛型,可以传入任何类型的value 
     */ 
    public <T> void addList(BloomFilterHelper<T> bloomFilterHelper, String key, List<T> valueList) { 
        redisTemplate.executePipelined(new RedisCallback<Long>() { 
            @Override 
            public Long doInRedis(RedisConnection connection) throws DataAccessException { 
                connection.openPipeline(); 
                for (T value : valueList) { 
                    int[] offset = bloomFilterHelper.murmurHashOffset(value); 
                    for (int i : offset) { 
                        connection.setBit(key.getBytes(), i, true); 
                    } 
                } 
                return null; 
            } 
        }); 
    } 
 
    /** 
     * 根据给定的布隆过滤器判断值是否存在 
     * 
     * @param bloomFilterHelper 布隆过滤器对象 
     * @param key               KEY 
     * @param value             值 
     * @param <T>               泛型,可以传入任何类型的value 
     * @return 是否存在 
     */ 
    public <T> boolean contains(BloomFilterHelper<T> bloomFilterHelper, String key, T value) { 
        int[] offset = bloomFilterHelper.murmurHashOffset(value); 
        for (int i : offset) { 
            if (!redisTemplate.opsForValue().getBit(key, i)) { 
                return false; 
            } 
        } 
        return true; 
    } 
}

使用

public static void main(String[] args) { 
        RedisBloomFilter redisBloomFilter = new RedisBloomFilter(); 
        int expectedInsertions = 1000; 
        double fpp = 0.1; 
        redisBloomFilter.delete("bloom"); 
        BloomFilterHelper<CharSequence> bloomFilterHelper = new BloomFilterHelper<>(Funnels.stringFunnel(Charset.defaultCharset()), expectedInsertions, fpp); 
        int j = 0; 
        // 添加100个元素 
        List<String> valueList = new ArrayList<>(); 
        for (int i = 0; i < 100; i++) { 
            valueList.add(i + ""); 
        } 
        long beginTime = System.currentTimeMillis(); 
        redisBloomFilter.addList(bloomFilterHelper, "bloom", valueList); 
        long costMs = System.currentTimeMillis() - beginTime; 
        log.info("布隆过滤器添加{}个值,耗时:{}ms", 100, costMs); 
        for (int i = 0; i < 1000; i++) { 
            boolean result = redisBloomFilter.contains(bloomFilterHelper, "bloom", i + ""); 
            if (!result) { 
                j++; 
            } 
        } 
        log.info("漏掉了{}个,验证结果耗时:{}ms", j, System.currentTimeMillis() - beginTime); 
    } 

注意这里用的是addList,它的底层是pipelining管道,而add方法的底层是一个个for循环的setBit,这样的速度效率是很慢的,但是他能有返回值,知道是否插入成功,而pipelining是不知道的,所以具体选择用哪一种方法看你的业务场景,以及需要插入的速度决定。

布隆过滤器工作位置

第一步是将数据库所有的数据加载到布隆过滤器。第二步当有请求来的时候先去布隆过滤器查询,如果bf说没有,第三步直接返回。如果bf说有,在往下走之前的流程。
应用的话,比如鉴权服务,当用户登录的时候可以先用布隆过滤器判断下,而不是直接去redis、数据库查。

布隆过滤器的其他应用场景

  • 网页爬虫对URL去重,避免爬取相同的 URL 地址;
  • 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱;
  • Google Chrome 使用布隆过滤器识别恶意 URL;
  • Medium 使用布隆过滤器避免推荐给用户已经读过的文章;
  • Google BigTable,Apache HBbase 和 Apache Cassandra使用布隆过滤器减少对不存在的行和列的查找。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值