天天看點

Bloom Filter 布隆過濾器

目錄

      • Bloom Filter簡介
      • 布隆過濾器的原理
      • 布隆過濾器的實作
        • 使用guava實作
        • 使用redis實作

Bloom Filter簡介

Bloom Filter 布隆過濾器,由一個叫布隆的小夥子提出,故而用他的名字來命名,可以判斷元素是否在指定集合中。

常見的應用場景

  • 避免緩存擊穿
  • 爬蟲:過濾新抓取到的url,已抓取存儲的就不再處理
  • 黑白名單:過濾垃圾郵件(檢測發件郵箱是否在垃圾郵箱集合中),攔截騷擾電話(檢測手機号是否在指定号碼庫中)等等
  • 過濾使用者:已簽到、未簽到,是否為新使用者、是否活躍。

優點

  • 使用二進制向量(數組),記憶體占用極少,空間效率高,存儲集合占用的空間比哈希表小得多
  • 存儲、查詢元素效率高,查詢花費的時間比一般算法少得多

缺點

  • 有一定的誤判率。可以判斷某個資料一定不在集合中或者可能在集合中(也可能不在集合中),可能在集合中(也可能不在)這是必然事件,肯定是正确判斷;而一定在集合中則可能存在誤判,布隆過濾器判定該資料一定不在集合中,但實際可能在集合中。
  • 删除困難,加載集合後難以删除集合中的元素

bloom filter能做到時間、空間上的高效,是以犧牲判斷的準确率、删除的便利性為代價的。

布隆過濾器的原理

布隆過濾器由一串很長的二進制向量和一系列随機映射函數組成,二進制向量可以将看做一個二進制數組,存放的元素是0、1,初始值預設為0。

當一個元素被加入集合時,通過K個散列函數将這個元素映射成一個位數組中的K個點,把它們置為1。檢索時,如果這些點有任何一個0,則被檢元素一定不在集合中;如果都是1,則被檢元素可能在集合中。

Bloom Filter跟單哈希函數Bit-Map不同之處在于:Bloom Filter使用了k個哈希函數,每個資料跟k個bit對應。進而降低了沖突的機率。

使用bloom filter時,需要預估集合資料量n、确定期望的誤判率fpp。誤判指的是布隆過濾器判斷為一定不在集合中,而實際可能在集合中。

布隆過濾器的實作

布隆過濾器隻是一個理論,有多種實作方式,常見的方式有2種

  • 使用google開源的guava實作
  • 使用redis實作

guava使用本地記憶體存儲資料,如果集合元素數量級較大,會占用很大一部分堆記憶體,此時可以考慮使用專業的記憶體型伺服器redis,但與redis互動有網絡通信的時間開銷,初始化布隆過濾器時添加集合元素極慢,資料判斷也比guava慢一些。

使用guava實作

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1.1-jre</version>
</dependency> 
           
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

/**
 * 使用Guava實作的BloomFilter
 */
public class GuavaBloomFilter {

    /**
     * 預計插入次數(集合中元素的預估數量),此處為 1百萬
     */
    private static final int expectedInsertions = 1000000;

    /**
     * 期待的誤判率,隻能為 (0,1) 上的小數,預設時預設為 0.03 即 3% 判斷100個資料大概有3個會誤判
     * 數值越大,對判斷結果的準确性要求越低,判斷速度越快,誤判發生的可能性越大、發生的誤判數量越多
     */
    private static final double fpp = 0.001;

    /**
     * 布隆過濾器,泛型指定集合元素的類型
     */
    // private static BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), expectedInsertions);
    private static BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), expectedInsertions, fpp);

    public static void main(String[] args) {
        //加載集合元素
        for (int i = 0; i < expectedInsertions; i++) {
            bf.put(i);
        }
        System.out.println("集合元素加載完畢");

        //判斷檢測
        int errCount = 0;
        for (int i = 0; i < expectedInsertions + 1000; i++) {
            //mightContain()判斷元素是否可能在集合中,false——一定不在集合中(可能為誤判),true——可能在集合中也可能不在
            if (!bf.mightContain(i)) {
                System.out.printf("%d可能為誤判\n", i);
                errCount++;
            }
        }
        System.out.printf("判斷檢測完畢,可能的誤判次數為%d", errCount);
    }

}
           

使用redis實作

redis可通過Bitmap實作布隆過濾器,Bitmap是在字元串類型(Simple Dynamic String,SDS)之上定義的與比特相關的一系列操作,SDS作為bit數組,redis提供了setbit、getbit、bitcount等指令來操作二進制位。

jedis、spring data redis屬于操作redis的基礎類庫,引入基礎類庫自己實作布隆過濾器很麻煩,可以使用現成的輪子Redisson。Redisson是java中操作redis的一個類庫,提供了更上層的封裝,功能強大。

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.16.1</version>
</dependency>
           
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

/**
 * 基于redis實作的布隆過濾器
 */
public class RedissonBloomFilter {

    /**
     * 預計插入次數(集合中元素的預估數量),此處為 1萬
     */
    private static final long expectedInsertions = 10000L;

    /**
     * 期待的誤判率,(0,1)上的小數
     * 數值越大,對判斷結果的準确性要求越低,判斷速度越快,誤判發生的可能性越大、發生的誤判數量越多
     */
    private static final double falseProbability = 0.001;

    /**
     * 擷取RedissonClient
     */
    public static RedissonClient getRedissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }

    public static void main(String[] args) {
        RedissonClient redissonClient = getRedissonClient();
        //會把布隆過濾器存儲為一個鍵值對,所有的元素作為一個string進行儲存,參數指定key的名稱,泛型指定集合元素的類型
        RBloomFilter<Integer> bf = redissonClient.getBloomFilter("xxx");
        //初始化布隆過濾器,指定預計插入次數、誤判率
        bf.tryInit(expectedInsertions, falseProbability);

        //加載集合元素
        for (int i = 0; i < expectedInsertions; i++) {
            bf.add(i);
        }
        System.out.println("集合元素加載完畢");

        //判斷檢測
        int errCount = 0;
        for (int i = 0; i < expectedInsertions + 1000; i++) {
            //contains判斷元素是否可能在集合中,false——一定不在集合中(可能為誤判),true——可能在集合中也可能不在
            if (!bf.contains(i)) {
                System.out.printf("%d可能為誤判\n", i);
                errCount++;
            }
        }
        System.out.printf("判斷檢測完畢,可能的誤判次數為%d", errCount);
    }

}
           

以上擷取的RedissonClient是單機版redis的,如果是redis叢集,可以參考https://blog.csdn.net/chy_18883701161/article/details/106380296

繼續閱讀