使用redis實現一個Bloom Filter(布隆過濾)

前言:

最近項目中須要對請求去重,想到的解決方案有2種:redis

  • 數據庫:成本較高,數據量多的話,還要分庫分表,查詢效率低
  • 布隆過濾:使用簡單、成本低,只須要佔用很小的內存空間,而且能夠設置過時時間;適合大數據量下的去重處理

Bloom Filter 概念

布隆過濾器(英語:Bloom Filter)是1970年由布隆提出的。它其實是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器能夠用於檢索一個元素是否在一個集合中。它的優勢是空間效率和查詢時間都遠遠超過通常的算法,缺點是有必定的誤識別率和刪除困難-->摘自維基百科算法

原理:

當一個元素被加入集合時,經過K個散列函數將這個元素映射成一個位數組中的K個點,把它們置爲1。檢索時,咱們只要看看這些點是否是都是1就(大約)知道集合中有沒有它了:若是這些點有任何一個0,則被檢元素必定不在;若是都是1,則被檢元素極可能在。這就是布隆過濾器的基本思想數據庫

由上面的概念可知實現一個布隆過濾須要2個基本條件:編程

  1. 二進制向量(位):引入redis的bit
  2. 合適的散列函數:參考guava的實現

Redis中 的bit

bit是計算機中的最小單位,值是0或1。redis中,經過設置指定offset的值,將其設置位1,表明該位置已經被佔用segmentfault

注意:offset 參數必須大於或等於 0 ,小於 2^32 (bit 映射被限制在 512 MB 以內)。數組

API:

  1. SETBIT key offset value:對key設置指定offset上的值
  2. GETBIT key offset:獲取key指定offset上對應的值

散列函數:

  1. 利用guava實現的64位哈希映射函數把對象key打散成二進制字節數組
  2. 再對字節數組分別取低八位和高八位字節進行hash,獲得2個hash值
  3. 利用hash值,2次取模獲取offset,對redis的bit位設1

到這裏,一個簡易版的布隆過濾就實現了,下面上代碼。bash

添加元素

public boolean add(String key) {
        //獲取字節數據
        byte[] bytes=Hashing.murmur3_128().hashString(key,Charset.forName("UTF-8")).asBytes();
        //hash1函數:低8位
        long hash1 = lowereight(bytes);
        //hash2函數 :高8位
        long hash2 = uppereight(bytes);

        long combinedHash = hash1;
        //2次hash
        for (int i = 0; i < 2; i++) {
            //對hash值取模:先取正,再取模
            long offeset = (combinedHash & Long.MAX_VALUE) % Integer.MAX_VALUE;
            //判斷指定位置是否已經有值
            if (RedisClient.getbit(name,(int) offeset)) {
                return false;
            }
            //設值爲1
            RedisClient.setbit(name,(int) offeset);
            combinedHash += hash2;
        }
        return true;
    }
複製代碼
//低8位:guava的實現
    private long lowereight(byte[] bytes) {
        return Longs.fromByteArray(bytes);
    }
    //高8位:guava的實現
    private long uppereight(byte[] bytes) {
        return Longs.fromBytes(
                bytes[15], bytes[14], bytes[13], bytes[12], bytes[11], bytes[10], bytes[9], bytes[8]);
    }
複製代碼

判斷元素是否存在:

和set的過程一致,這裏只要2次hash值中的一個位置上有元素(值爲1),那麼這個元素就已經存在了函數

public boolean contain(String key) {
        byte[] bytes=Hashing.murmur3_128().hashString(key,Charset.forName("UTF-8")).asBytes();
        long hash1 = lowereight(bytes);
        long hash2 = uppereight(bytes);
        long combinedHash = hash1;
        for (int i = 0; i < 2; i++) {
            long offeset = (combinedHash & Long.MAX_VALUE) % Integer.MAX_VALUE;
            if (RedisClient.getbit(name,(int) offeset)) {
                return true;
            }
            combinedHash += hash2;
        }
        return false;
    }
複製代碼

測試結果:

10萬記錄,13個誤判測試

@Test
    public void test3(){
        int count=0;
        SBloomFilter bloomFilter=SBloomFilter.getSBloomFilter("selrain");
        for(int i=0;i<100000;i++){
            String key="test"+i;
            if(bloomFilter.contain(key)){
                count++;
            }else {
                bloomFilter.add(key);
            }
        }
        System.out.println(count);
    }
複製代碼

思考:

  1. 通常的布隆過率會有2個入參:指望插入次數(number),誤判率(false positive probability.),具體hash的次數是經過這2個值計算出來,我這裏固定了2次hash;另外redis的bit我也沒看到有初始化bit數組的大小的函數,有知道的麼
  2. 跑數據我發現了佔用了200M+內存,內存佔用仍是很高的,最終用了redisson實現的方案(由於佔用的內存更小,大概10M+),不知道作了什麼優化?感興趣的能夠看看

參考:

segmentfault.com/a/119000001…大數據

最後:

小尾巴走一波,歡迎關注個人公衆號,不按期分享編程、投資、生活方面的感悟:)

相關文章
相關標籤/搜索