懶才製造生產力 - 布隆過濾器實現去重工具

前言

最近有個段子,程序員不要拷貝代碼,而是要一行一行的從新抄過去。好嘛,段子歸段子,筆主始終以爲 think more, code less,多思考,能少寫的代碼儘可能少寫,從而也與標題呼應上,懶才製造生產力。繼上次寫的 excel 工具類以後,此次又來給你們 show 一下筆主是如何釋放生產力的。java

需求背景

項目中常常會有觸達需求,簡單來講就是羣發微信公衆號消息。這裏有個剛需就是去重,已經下發過的用戶再也不下發,多個標籤用戶之間也可能會存在重複的用戶 ,這些都須要去重。git

解決方案

  • 兩層循環遍歷:最 low 的辦法不就是套兩層循環遍歷,時間複雜度 n 方。
  • HashSet:稍微有點追求的小夥伴可能會想到使用 HashSet 去重,沒錯,HashSet 確實是個不錯的選擇,時間複雜度很低。可是面對海量數據,HashSet 彷佛就不太適合了,HashSet 有個弱點就是,空間利用率低,並且元素佔用的空間也相對較大。
  • 布隆過濾器:針對海量數據去重場景,布隆過濾器應運而生,關於布隆過濾器網上不少博客都說的很詳細。

做者下面就簡單說一下布隆過濾器的關鍵點,把主要精力放在如何應用到實際項目中。程序員

布隆過濾器

  • 使用場景:判斷某個元素是否在海量數據之中。
  • 存儲結構:使用 比特 數組存儲。
  • 添加元素
    1. 每添加一個元素,分別對該元素進行 k 次不一樣的哈希。
    2. 將上面 k 個哈希的結果對應於數組的位置設爲1。
  • 檢查元素
    1. 求出要查詢的元素的 k 個哈希值。
    2. 若是k個位置有一個爲0,則確定不在集合中。
    3. 若是k個位置所有爲1,則可能在集合中。

爲何須要進行 k 次哈希?

主要是爲了下降哈希衝突引起的偏差,對於 HashMap 來講,哈希衝突的時候,會用鏈表或者是紅黑樹將全部衝突的元素都保存起來。可是對於布隆過濾器不能也不須要把衝突的 key 用鏈表鏈接起來,由於他只須要判斷 key 是否存在。github

如何計算布隆過濾器佔用的空間大小?

可使用這個在線計算工具:Bloom Filter Calculatorweb

去重工具使用姿式

在介紹具體的實現過程以前,先看看做者手擼的去重工具的正確使用姿式。數據庫

1. 配置布隆過濾器信息

根據約定將去重器的配置文件放在 deduplication.properties 下。數組

bloomList=goods,wechat
tableInfo=goods:goods_deduplication,wechat:wechat_deduplication
expectedInsertions=goods:200000
複製代碼
  • bloomList:布隆過濾器 bean 的名稱,一個去重業務對應一個值,多個值之間使用逗號分割。
  • tableInfo:布隆過濾器對應的數據表名,不配置會使用默認值。
  • expectedInsertions:布隆過濾器的預計數據量,這個對數據的準確性有較大的影響,須要根據實際業務進行評估,默認值爲 10000000。

2. 去重器使用

配置完上面的信息以後,就可使用去重器了,使用姿式以下圖所示,核心代碼就一行。傳入待比較的 List 和業務對應的布隆過濾器 bean 名稱,返回目前還不存在的記錄,且會將該記錄入庫,下次再進行去重,該記錄就不會再出現了。微信

測試程序: 多線程

運行程序以前,已存在的數據以下,identifier 爲 十二、1六、13 的記錄已經存在了。 less

程序運行結果:

數據庫狀態:

能夠看到成功將 十一、1四、15 的記錄返回且成功入庫。

去重器源碼分析

下面主要講的是去重器實現的核心步驟,在分析代碼以前,先把 ✨去重器源碼地址✨ 貼出來,嘻嘻嘻,歡迎你們來 star。該工具的核心代碼都在 deduplication 這個包下面。

設計目標

傳入任意的惟一標識 List,或者是指定惟一標識的 CSV 文件進行去重,對任意的業務均適用。

實現思路

  1. 建立布隆過濾:針對某個去重業務須要一個布隆過濾器與之對應。
  2. 初始化布隆過濾器:每一個布隆過濾器的初始化數據從對應的數據庫表中取。
  3. 使用布隆過濾器:使用初始化事後的布隆過濾器判斷元素是否存在,存在入庫。

步驟很簡單,可是想要實現的優雅點,使用起來方便一點,仍是須要 think 一下。

建立布隆過濾

這裏做者使用的布隆過濾器是 guava 提供的實現,並無重複造輪子,畢竟 Google 出品。

這裏主要解決的問題點有兩個:

  1. 這個布隆過濾須要是建立一次,全局有效,且是單例,這個問題很好解決,把該布隆過濾器交給 Spring 容器管理便可,拍拍手,乾淨得很。
  2. 傳統建立 guava 布隆過濾器是使用 BloomFilter#create() 方法,可是咱們的目標是懶!一懶到底!沒理由新增一個去重業務,咱們就去翻代碼,new 一個布隆過濾器出來吧。因此這裏咱們須要向 Spring 容器動態註冊 bean 的能力,可使用 Spring 提供的 BeanDefinitionRegistryPostProcessor 接口 來實現這個功能。

建立布隆過濾核心代碼:

上面標出了四個關鍵點:

  • 【1】 從配置文件中取出全部的 bean 名稱。
  • 【2】 循環 bean 列表,動態註冊 bean。
  • 【3】初始化布隆過濾器初始化狀態的標識爲 false,標識該布隆過濾還處於 not ready 狀態,不能開放使用。
  • 【4】往 Spring 容器中註冊了一個名爲 beanId 的 BloomFilter 。

通過上面的步驟,咱們已經建立好了全部的布隆過濾器。

初始化布隆過濾器

這一步主要是從數據庫中查詢數據,而後塞到布隆過濾器中。可能有朋友會問,爲何還要把初始化這一步單獨拎出來?直接在建立bean的時候初始化不就好了。主要是由於在動態註冊 bean 的時候,容器的上下文環境尚未準備好階段。在容器中還沒法獲取到其餘的 bean 。因此咱們只能把初始化放在這一階段來作。

在這裏做者使用了 ApplicationListener 監聽,他能夠監聽 SpringBoot 應用的生命週期,做者在這裏監聽了 SpringBoot 啓動完成以後的事件,把對布隆過濾器的初始化工做放到這裏面來進行。


DeduplicationApplicationListener 監聽器核心代碼:

  • 【1】選擇監聽應用啓動完成以後的事件。
  • 【2】取出全部的布隆過濾器名稱。
  • 【3】啓動線程調用 initBloomFilter() 初始化每一個布隆過濾器。

initBloomFilter 方法:

  • 【1】根據傳入的布隆過濾器名稱獲取 bean 實例,這裏主要是經過 ApplicationContextAware 動態獲取 bean 實例。
  • 【2】根據傳入的布隆過濾器名稱獲取對應的數據庫表名。
  • 【3】若是當前數據庫表還不存在,建表後當即返回。
  • 【4】不然調用 fillBloomFilter 方法。

fillBloomFilter 方法:

這裏面主要的工做就是將數據庫中的數據塞到布隆過濾器中,一般這一步查詢出來的數據會很是的多,這裏選擇了將查詢任務切分,多線程查詢,而後再合併子任務,沒錯!就是借鑑 MapReduce 的思想。

數據塞完以後,該布隆過濾器就能夠對外開放使用了,因此會將該布隆過濾器對應的初始化狀態置爲 true。

通過上面的步驟,已經將布隆過濾器初始化完成了,下面對布隆過濾器進行必定的封裝就可使用了。

封裝布隆過濾器

這一步主要是對布隆過濾器進行封裝,使之用起來更加的方便,這裏主要支持兩種類型的數據源去重,分別是 List 和 CSV 文件。這裏都是批量去重,入庫也是批量入庫。若是須要單條記錄去重,例如過濾黑名單,就須要本身封裝對應的業務了,由於畢竟不一樣用戶的業務仍是不太同樣的。

對外暴露的兩個方法:

go 是一個重載函數,僅對內部使用:

  • 【1】查找 List 中不存在的元素。
  • 【2】查找 CSV 文件中不存在的記錄。
  • 【3】批量入庫。

這裏再來看看 findNotExistRecordFromList 方法。很簡單,遍歷傳入的 list,若是布隆過濾器判斷爲還不存在,加入到結果集中返回。

寫在最後

關於去重器的核心步驟就介紹到這裏啦,對於實現的細節,感興趣的朋友能夠到 github 上把代碼 fork 下來看一哈,有問題歡迎隨時與我交流。

打波廣告嘛

✨github:cranberry✨ 是做者最近在寫的一個項目,主要是對 java web 開發過程當中的一些常見問題的實現與分析,後續也會持續更新,但願對你們會有幫助,感受不錯也能夠給作個 star 一個哦!

相關文章
相關標籤/搜索