Redis布隆過濾器(Bloom Filter)

1. 概念

Bloom Filter可以理解為是一個m位的數(shù)組翩剪,它有k個相互獨立的哈希函數(shù)腕柜。每當新來一個元素時膀篮,會使用這些哈希函數(shù)分別計算毯焕,將對應(yīng)位置置為1衍腥。查詢時也要進行k次哈希運算,如果對應(yīng)的k個位置都為1纳猫,則表明元素可能存在婆咸。

Bloom Filter示意圖:
假如當前的過濾器中已經(jīng)記錄了1、10芜辕,此時判斷3是否存在尚骄,匹配之后發(fā)現(xiàn)并不存在



(圖中的方格就代表著數(shù)組,每個數(shù)字指向二位數(shù)組的線就代表一個哈希元素侵续。)

注:布隆過濾器只能判斷某個值一定不存在倔丈,無法判斷某個值一定存在憨闰。

2. 具體使用

2.1 google.guava

Google在guava包中提供了一個布隆過濾器的實現(xiàn)——com.google.common.hash.BloomFilter
put(T object) //往布隆過濾器中存入object
mightContain(T object) //判斷object是否存在

直接貼代碼,首先是pom

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1.1-jre</version>
</dependency>

首先在配置類中創(chuàng)建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<String> 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

結(jié)果:

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的位集宏邮,也就是上文所說的數(shù)組 */
private final LockFreeBitArray bits;

/** 表示哈希函數(shù)的個數(shù) */
private final int numHashFunctions;

/** 把任意類型的數(shù)據(jù)轉(zhuǎn)換為Java基本數(shù)據(jù)類型泽示,最終轉(zhuǎn)化為byte數(shù)組 */
private final Funnel<? super T> funnel;

/** 內(nèi)部接口,將元素T映射到numHashFunctions位索引的策略 */
private final Strategy strategy;

然后看一下4個構(gòu)造方法

public static <T> BloomFilter<T> create(
    Funnel<? super T> funnel, int expectedInsertions, double fpp) {
    return create(funnel, (long) expectedInsertions, fpp);
}

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

public static <T> BloomFilter<T> create(Funnel<? super T> funnel, int expectedInsertions) {
    return create(funnel, (long) expectedInsertions);
}

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

四個構(gòu)造方法沒太大的區(qū)別蜀铲,最后都會指向同一個create方法
這個方法中接收了4個參數(shù):funnel(輸入的數(shù)據(jù))边琉,expectedInsertions(預(yù)計插入的元素總數(shù)),fpp(期望誤判率)记劝,strategy(實現(xiàn)Strategy的實例,參考上面的公共構(gòu)造方法族扰,默認傳遞的是BloomFilterStrategies.MURMUR128_MITZ_64)

@VisibleForTesting
static <T> BloomFilter<T> create(
    Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
    //各種參數(shù)校驗
    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!
     */
    //計算數(shù)組長度
    long numBits = optimalNumOfBits(expectedInsertions, fpp);
    //計算出每個元素需要哈希方法的個數(shù)
    int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
    try {
      //創(chuàng)建BloomFilter對象
      return new BloomFilter<T>(new LockFreeBitArray(numBits), numHashFunctions, funnel, strategy);
    } catch (IllegalArgumentException e) {
      throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", e);
    }
}

最后再看一下BloomFilter這個構(gòu)造方法厌丑,很簡單,檢查完了之后給4個成員變量賦值

/** Creates a BloomFilter. */
private BloomFilter(
    LockFreeBitArray bits, int numHashFunctions, Funnel<? super T> 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);
}

都是調(diào)用的成員變量strategy的方法渔呵,我們知道構(gòu)造函數(shù)的strategy的參數(shù)都是BloomFilterStrategies.MURMUR128_MITZ_64這個枚舉值怒竿,貼一下代碼

MURMUR128_MITZ_64() {
    @Override
    public <T> boolean put(
        T object, Funnel<? super T> 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 <T> boolean mightContain(
        T object, Funnel<? super T> 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位的字節(jié)數(shù)組录豺,然后高低分別取8個字節(jié)(64位)創(chuàng)建2個long型整數(shù)hash1朦肘,hash2作為哈希值。循環(huán)體內(nèi)采用了2個函數(shù)模擬其他函數(shù)的思想双饥,即上文提到的gi(x) = h1(x) + ih2(x) 媒抠,這相當于每次累加hash2,然后通過基于bitSize取模的方式在bit數(shù)組中索引咏花。

這里之所以要和Long.MAX_VALUE進行按位與的操作趴生,是因為在除數(shù)和被除數(shù)符號不一致的情況下計算所得的結(jié)果是有差別的,在程序語言里昏翰,“%”準確來說是取余運算(C苍匆,C++和Java均如此,python是取模)棚菊,如-5%3=-2浸踩,而取模的數(shù)學(xué)定義是x mod y=x-y[x/y](向下取整),所以-5 mod 3= -5-3*(-2)=1窍株,因此當哈希值為負數(shù)的時候民轴,其取余的結(jié)果為負(bitSize始終為正數(shù))攻柠,這樣就不方便在bit數(shù)組中取值,因此通過Long.MAX_VALUE(二進制為0111…1111)后裸,直接將開頭的符號位去掉瑰钮,從而轉(zhuǎn)變?yōu)檎龜?shù)。當然也可以取絕對值微驶,在另一個MURMUR128_MITZ_32的實現(xiàn)中就是這么做的浪谴。

在put方法中,先是將索引位置上的二進制置為1因苹,然后用bitsChanged記錄插入結(jié)果苟耻,如果返回true表明沒有重復(fù)插入成功,而mightContain方法則是將索引位置上的數(shù)值取出扶檐,并判斷是否為0凶杖,只要其中出現(xiàn)一個0,那么立即判斷為不存在款筑。

這種使用方式屬于單機版的布隆過濾器的使用智蝠,在分布式場景下并不試用,所以需要redis來做一個分布式場景下的布隆過濾器

總結(jié):

  1. BloomFilter類就是利用公式完成對參數(shù)的估算奈梳,創(chuàng)建了一個本地的BitArray數(shù)組(一個Long類型的數(shù)組杈湾,長度m)和需要哈希方法的次數(shù)k。
  2. 之后利用MURMUR128_MITZ_64這個枚舉值中的方法來進行運算攘须,在put方法中漆撞,利用計算出來的k個數(shù)值。在BitArray中于宙,以這k個數(shù)值為下標浮驳,將原有為0的值修改為1。
  3. mightContain方法中限煞,跟put方法同理抹恳,計算出k個數(shù)值,在BitArray中判斷這些數(shù)值為下標的值是否為0署驻,只要出現(xiàn)一個0則返回false奋献。
  4. 僅適合一個節(jié)點的使用,因為分布式中每個節(jié)點的BloomFilter的bean都是獨立的旺上。
2.3 利用Redis Bitmaps進行重構(gòu)

大致思路:創(chuàng)建一個新的BloomFilter瓶蚂,4個成員變量中,將Strategy這個變量改為使用Redis BitMap宣吱,其余的3個成員變量及計算方式保留窃这。將MURMUR128_MITZ_64中的Hashing.murmur3_128()方法拿出來,得到計算出來的k個下標征候,之后循環(huán)這些下標將BitMap中的這些值修改為1杭攻。

了解下Redis Bitmaps的結(jié)構(gòu)祟敛,推薦一篇不錯的博客:https://www.cnblogs.com/54chensongxia/p/13794391.html

下面貼代碼。
首先創(chuàng)建一個BloomFilter類

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

public class BloomFilterHelper<T> {

    private int numHashFunctions;

    private int bitSize;

    private Funnel<T> funnel;

    public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) {
        Preconditions.checkArgument(funnel != null, "funnel不能為空");
        this.funnel = funnel;
        // 計算bit數(shù)組長度
        bitSize = optimalNumOfBits(expectedInsertions, fpp);
        // 計算hash方法執(zhí)行次數(shù)
        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數(shù)組長度
     * @param n 預(yù)計插入的元素總數(shù)
     * @param p 期望誤判率
     * @return
     */
    private int optimalNumOfBits(long n, double p) {
        if (p == 0) {
            // 設(shè)定最小期望長度
            p = Double.MIN_VALUE;
        }
        int sizeOfBitArray = (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
        return sizeOfBitArray;
    }

    /**
     * 計算hash方法執(zhí)行次數(shù)
     * @param n 預(yù)計插入的元素總數(shù)
     * @param m bit數(shù)組長度
     * @return
     */
    private int optimalNumOfHashFunctions(long n, long m) {
        int countOfHash = Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
        return countOfHash;
    }
}

接下來就是初始化一個上面自己實現(xiàn)的布隆過濾器類的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<String> initBloomFilterHelper() {
        BloomFilterHelper<String> bloomFilter = new BloomFilterHelper((Funnel<String>) (from, into) -> {
            into.putString(from, Charsets.UTF_8);
        }, 1000000, 0.0000001);
        return bloomFilter;
    }
}

之后創(chuàng)建BloomFilterUtil(主要操作的類)兆解,利用BloomFilterHelper的murmurHashOffset計算出哈希方法的數(shù)組馆铁,循環(huán)數(shù)組以每個值為下標,將Redis BitMap中這些值設(shè)置為1锅睛。(我用的是jedis埠巨,具體方法不貼了網(wǎng)上有很多)

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 <T> void addBloomFilter(String key, T value) {
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
            redisUtil.setbit(key, i, true);
        }
    }

    public <T> 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

輸出結(jié)果:

13333333333是否存在:false
插入:13333333333
13333333334是否存在:false
插入:13333333334
13333333335是否存在:false
插入:13333333335
13333333336是否存在:false
插入:13333333336
13333333337是否存在:false
插入:13333333337
13333333335是否存在:true
13333333337是否存在:true
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市现拒,隨后出現(xiàn)的幾起案子辣垒,更是在濱河造成了極大的恐慌,老刑警劉巖印蔬,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件勋桶,死亡現(xiàn)場離奇詭異,居然都是意外死亡扛点,警方通過查閱死者的電腦和手機哥遮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來陵究,“玉大人,你說我怎么就攤上這事奥帘⊥剩” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵寨蹋,是天一觀的道長松蒜。 經(jīng)常有香客問我,道長已旧,這世上最難降的妖魔是什么秸苗? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮运褪,結(jié)果婚禮上惊楼,老公的妹妹穿的比我還像新娘。我一直安慰自己秸讹,他們只是感情好檀咙,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著璃诀,像睡著了一般弧可。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上劣欢,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天棕诵,我揣著相機與錄音裁良,去河邊找鬼。 笑死校套,一個胖子當著我的面吹牛价脾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播搔确,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼彼棍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了膳算?” 一聲冷哼從身側(cè)響起座硕,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎涕蜂,沒想到半個月后华匾,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡机隙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年蜘拉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片有鹿。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡旭旭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出葱跋,到底是詐尸還是另有隱情持寄,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布娱俺,位于F島的核電站稍味,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏荠卷。R本人自食惡果不足惜模庐,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望油宜。 院中可真熱鬧掂碱,春花似錦、人聲如沸验庙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽粪薛。三九已至悴了,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背湃交。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工熟空, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人搞莺。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓息罗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親才沧。 傳聞我的和親對象是個殘疾皇子迈喉,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容