基于Redis的BloomFilter 實(shí)操

BloomFilter

Bloom Filter 是一種多哈希函數(shù)映射的快速查找算法, 通常應(yīng)用于大數(shù)據(jù)和高并發(fā)下的數(shù)據(jù)去重處理哨颂, 但是又不對(duì)準(zhǔn)確率有嚴(yán)格的100%的正確率喷市。
Bloom Filter過(guò)濾器的工作步驟為:

  1. 預(yù)設(shè)m 位長(zhǎng)度的BitSet對(duì)象。
  2. 將去重對(duì)象經(jīng)過(guò)K次hash威恼,判斷每次hash后的值在BitSet中對(duì)應(yīng)索引位置的值是不是為1品姓。
  3. 如果步驟2中每次獲取的值都是1寝并, 那么可以判定當(dāng)前對(duì)象已經(jīng)已經(jīng)被存儲(chǔ)過(guò),可以被去重或者過(guò)濾腹备。

參數(shù)設(shè)計(jì)

通過(guò)上面的描述衬潦, 相信大家對(duì)Bloom Filter有了大致的了解, 現(xiàn)在我們來(lái)列出下一個(gè)Bloom Filter需要的參數(shù):

  • 插入的的對(duì)象個(gè)數(shù) n
  • Bloom Filter 的誤判率 P
  • hash 函數(shù)的個(gè)數(shù) K
  • BitSet的位數(shù) m

在實(shí)際的項(xiàng)目中植酥, 欲插入的對(duì)象數(shù)目和誤判率我們都可以預(yù)估和設(shè)定镀岛, 那么現(xiàn)在看來(lái)最重要的是如何設(shè)定m和k的值。對(duì)于上述參數(shù)的設(shè)定和評(píng)估友驮, 都有計(jì)算公式:

誤判率 P(true):

P(true) \approx (1 - e^ \frac{-nk}{m})^k

Hash 函數(shù)的個(gè)數(shù) K:

f(k) = (1-a^{-k})^k
求導(dǎo)之后:
k= \frac{m}{n} \ln2 \approx 0.7\frac{m}{n}

BitArray數(shù)組的大小 m

通過(guò)聯(lián)立誤判率和hash 函數(shù)的k的兩個(gè)公式可以得到:
P(true)=(1-e^{-\ln2})^{\frac{m}{n}\ln2} \approx 0.6185\frac{m}{n}
通過(guò)上述公式可以求出:
m = -\frac{n\ln{P(true)}}{(\ln2)^2}

Redis

熟悉redis的人知道漂羊, redis中存在一種位存儲(chǔ), 這種為位存儲(chǔ)可以極大的降低redis的內(nèi)存卸留。位操作常用的命令為:

SETBIT KEY OFFSET VALUE 
GETBIT KEY OFFSET

這種結(jié)構(gòu)如果和BloomFilter 結(jié)合起來(lái)就可以實(shí)現(xiàn)分布式的布隆過(guò)濾器了拨与。
實(shí)現(xiàn)思路為:

  1. 對(duì)校驗(yàn)的對(duì)象做K次hash得到位移offset
  2. 調(diào)用getbit 命令檢查是不是每次返回的值都是1
  3. 如果返回K個(gè)1表示這個(gè)對(duì)象已經(jīng)被存儲(chǔ)過(guò)
  4. 如果沒(méi)有的話(huà), 可以對(duì)該對(duì)象進(jìn)行存儲(chǔ)

經(jīng)過(guò)上述講解艾猜, 流程和邏輯基本都差不多了,萬(wàn)事俱備開(kāi)始擼碼:
因?yàn)槲覀冊(cè)谑褂貌悸∵^(guò)濾器之前捻悯, 我們可以預(yù)先預(yù)估誤判率P和想要插入的個(gè)數(shù)n

計(jì)算獲bitMap預(yù)分配的長(zhǎng)度

從上面公式可以推算bit 的長(zhǎng)度匆赃, 但是需要注意的是公式計(jì)算出來(lái)的是浮點(diǎn)數(shù)

    /**
     * 計(jì)算bit數(shù)組的長(zhǎng)度,
     * m = -n * Math.log(p)/Math.pow(ln2,2)
     * @param n 插入條數(shù)
     * @param p 誤判概率
     */
    private int numOfBits(int 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;
    }

計(jì)算hash的次數(shù)

    /**
     * 計(jì)算hash方法執(zhí)行次數(shù)
     * k = m/n*ln2
     * @param n 插入的數(shù)據(jù)條數(shù)
     * @param m 數(shù)據(jù)位數(shù)
     */
    private int numberOfHashFunctions(long n, long m) {
        int countOfHash = Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
        return countOfHash;
    }

獲取hash函數(shù)計(jì)算之后的位移集合

這個(gè)hash函數(shù)采用的是guava中的murmur函數(shù)

   //hash函數(shù)的次數(shù)
    private int numHashFunctions;
    //bit長(zhǎng)度
    private int bitSize;

    private Funnel<T> funnel;

    private static int MAX_BIT_SIZE = 1 << 30;

    private static int DEFAULT_HASH = 3;

    public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) {
        Preconditions.checkArgument(funnel != null, "funnel不能為空");
        this.funnel = funnel;
        bitSize = Math.min(optimalNumOfBits(expectedInsertions, fpp),MAX_BIT_SIZE);
        numHashFunctions = Math.min(optimalNumOfHashFunctions(expectedInsertions, bitSize), DEFAULT_HASH);
    }
    /**
     * 計(jì)算hash函數(shù)之后的位移集合
     * @param value
     * @return
     */
    public List<long> murmurHashOffset(T value) {
        List<long> offsetList = new ArrayList<>(numHashFunctions);
        long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
        long hash2 =  (hash64 >>> 32);
        for (int i = 1; i <= numHashFunctions; i++) {
            long nextHash = hash64 + i * hash2;
            if (nextHash < 0) {
                nextHash = ~nextHash;
            }
            offsetList.add(nextHash % bitSize);
        }
        return offsetList;
    }

單機(jī)的布隆過(guò)濾器已經(jīng)建好了今缚, 接下來(lái)就是和redis整合了算柳, 由于可能會(huì)有多次的setbit操作,這樣可能會(huì)發(fā)生多次的網(wǎng)絡(luò)請(qǐng)求姓言, 所以考慮的是用lua腳本來(lái)執(zhí)行:

  private static final String GET_BIT_LUA = "for i=1,#ARGV\n" +
            "do\n" +
            "    local value =  redis.call(\"GETBIT\", KEYS[1], ARGV[i])\n" +
            "    if value == 0\n" +
            "    then\n" +
            "        return 0\n" +
            "    end\n" +
            "end\n" +
            "return 1";

    private static final String SET_BIT_LUA = "for i=1, #ARGV\n" +
            "do\n" +
            "    redis.call(\"SETBIT\",KEYS[1], ARGV[i],1)\n" +
            "end\n";

布隆過(guò)濾器的插入和判斷操作分別如下:

public static <T> void addByBloomFilter(IRedisHelper redisHelper, BloomFilterHelper<T> bloomFilterHelper, Object key, T value) {
        Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能為空");
        List<Long> offsetList = bloomFilterHelper.murmurHashOffset(value);
        if(CollectionUtils.isEmpty(offsetList)){
            return ;
        } 
       redisHelper.eval(routeKey, SET_BIT_LUA, Lists.newArrayList(key.getRawKey()), offsetList);
    }

    /**
     * 根據(jù)給定的布隆過(guò)濾器判斷值是否存在
     */
    public static <T> boolean includeByBloomFilter(IRedisHelper redisHelper, BloomFilterHelper<T> bloomFilterHelper, Object key, T value) {
        Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能為空");
        List<Long> offsetList = bloomFilterHelper.murmurHashOffset(value);
        if(CollectionUtils.isEmpty(offsetList)){
            return false;
        }

      String result = String.valueOf(eval);
      if("1".equalsIgnoreCase(result)){
        return true;
       }        
    return false;
    }

對(duì)于redis的bitmap 存在一個(gè)問(wèn)題瞬项,就是內(nèi)存初始化的問(wèn)題,
下面是來(lái)自官方的原話(huà):

 When setting the last possible bit (offset equal to 2^32 -1) and the string value stored at key does not yet 
hold a string value, or holds a small string value, Redis needs to allocate all intermediate memory which can
 block the server for some time. On a 2010 MacBook Pro, setting bit number 2^32 -1 (512MB allocation) 
takes ~300ms, setting bit number 2^30 -1 (128MB allocation) takes ~80ms, 
setting bit number 2^28 -1 (32MB allocation) takes ~30ms and setting bit number 2^26 -1 (8MB allocation) takes ~8ms.

如果bitmap的長(zhǎng)度是2^32的話(huà),可能需要300ms 分配內(nèi)存何荚, 2^30 需要80ms, 2^28需要30ms, 2&26只需要8ms, 假如項(xiàng)目需要對(duì)性能和延遲有要求囱淋, 那么如何分配這個(gè)bitmap是個(gè)需要考慮的問(wèn)題烟勋。

參考

1.https://zhuanlan.zhihu.com/p/140545941
2.https://redis.io/commands/setbit

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末乏冀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子筋夏,更是在濱河造成了極大的恐慌戒傻,老刑警劉巖税手,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異需纳,居然都是意外死亡芦倒,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)不翩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)兵扬,“玉大人麻裳,你說(shuō)我怎么就攤上這事≈苊梗” “怎么了掂器?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)俱箱。 經(jīng)常有香客問(wèn)我国瓮,道長(zhǎng),這世上最難降的妖魔是什么狞谱? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任乃摹,我火速辦了婚禮,結(jié)果婚禮上跟衅,老公的妹妹穿的比我還像新娘孵睬。我一直安慰自己,他們只是感情好伶跷,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布掰读。 她就那樣靜靜地躺著,像睡著了一般叭莫。 火紅的嫁衣襯著肌膚如雪蹈集。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天雇初,我揣著相機(jī)與錄音拢肆,去河邊找鬼。 笑死靖诗,一個(gè)胖子當(dāng)著我的面吹牛郭怪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播刊橘,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼鄙才,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了促绵?” 一聲冷哼從身側(cè)響起咒循,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎绞愚,沒(méi)想到半個(gè)月后叙甸,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡位衩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年裆蒸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片糖驴。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡僚祷,死狀恐怖佛致,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情辙谜,我是刑警寧澤俺榆,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站装哆,受9級(jí)特大地震影響罐脊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蜕琴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一萍桌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧凌简,春花似錦上炎、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至凸郑,卻和暖如春裳食,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背线椰。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留尘盼,地道東北人憨愉。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像卿捎,于是被迫代替她去往敵國(guó)和親配紫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

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