最近有個段子,程序員不要拷貝代碼,而是要一行一行的從新抄過去。好嘛,段子歸段子,筆主始終以爲
think more, code less
,多思考,能少寫的代碼儘可能少寫,從而也與標題呼應上,懶才製造生產力。繼上次寫的 excel 工具類以後,此次又來給你們 show 一下筆主是如何釋放生產力的。java
項目中常常會有觸達需求,簡單來講就是羣發微信公衆號消息。這裏有個剛需就是去重,已經下發過的用戶再也不下發,多個標籤用戶之間也可能會存在重複的用戶 ,這些都須要去重。git
兩層循環遍歷
:最 low 的辦法不就是套兩層循環遍歷,時間複雜度 n 方。HashSet
:稍微有點追求的小夥伴可能會想到使用 HashSet 去重,沒錯,HashSet 確實是個不錯的選擇,時間複雜度很低。可是面對海量數據,HashSet 彷佛就不太適合了,HashSet 有個弱點就是,空間利用率低,並且元素佔用的空間也相對較大。布隆過濾器
:針對海量數據去重場景,布隆過濾器應運而生,關於布隆過濾器網上不少博客都說的很詳細。做者下面就簡單說一下布隆過濾器的關鍵點,把主要精力放在如何應用到實際項目中。程序員
使用場景
:判斷某個元素是否在海量數據之中。存儲結構
:使用 比特
數組存儲。添加元素
:
檢查元素
:
主要是爲了下降哈希衝突引起的偏差,對於 HashMap 來講,哈希衝突的時候,會用鏈表或者是紅黑樹將全部衝突的元素都保存起來。可是對於布隆過濾器不能也不須要把衝突的 key 用鏈表鏈接起來,由於他只須要判斷 key 是否存在。github
可使用這個在線計算工具:Bloom Filter Calculatorweb
在介紹具體的實現過程以前,先看看做者手擼的去重工具的正確使用姿式。數據庫
根據約定將去重器的配置文件放在 deduplication.properties
下。數組
bloomList=goods,wechat
tableInfo=goods:goods_deduplication,wechat:wechat_deduplication
expectedInsertions=goods:200000
複製代碼
bloomList
:布隆過濾器 bean 的名稱,一個去重業務對應一個值,多個值之間使用逗號分割。tableInfo
:布隆過濾器對應的數據表名,不配置會使用默認值。expectedInsertions
:布隆過濾器的預計數據量,這個對數據的準確性有較大的影響,須要根據實際業務進行評估,默認值爲 10000000。配置完上面的信息以後,就可使用去重器了,使用姿式以下圖所示,核心代碼就一行。傳入待比較的 List 和業務對應的布隆過濾器 bean 名稱,返回目前還不存在的記錄,且會將該記錄入庫,下次再進行去重,該記錄就不會再出現了。微信
測試程序: 多線程
運行程序以前,已存在的數據以下,identifier 爲 十二、1六、13 的記錄已經存在了。 less
程序運行結果:
數據庫狀態:
能夠看到成功將 十一、1四、15 的記錄返回且成功入庫。
下面主要講的是去重器實現的核心步驟,在分析代碼以前,先把 ✨去重器源碼地址✨ 貼出來,嘻嘻嘻,歡迎你們來 star。該工具的核心代碼都在 deduplication 這個包下面。
傳入任意的惟一標識 List,或者是指定惟一標識的 CSV 文件進行去重,對任意的業務均適用。
建立布隆過濾
:針對某個去重業務須要一個布隆過濾器與之對應。初始化布隆過濾器
:每一個布隆過濾器的初始化數據從對應的數據庫表中取。使用布隆過濾器
:使用初始化事後的布隆過濾器判斷元素是否存在,存在入庫。步驟很簡單,可是想要實現的優雅點,使用起來方便一點,仍是須要 think 一下。
這裏做者使用的布隆過濾器是 guava 提供的實現,並無重複造輪子,畢竟 Google 出品。
這裏主要解決的問題點有兩個:
BloomFilter#create()
方法,可是咱們的目標是懶!一懶到底!沒理由新增一個去重業務,咱們就去翻代碼,new 一個布隆過濾器出來吧。因此這裏咱們須要向 Spring 容器動態註冊 bean
的能力,可使用 Spring 提供的 BeanDefinitionRegistryPostProcessor
接口 來實現這個功能。建立布隆過濾核心代碼:
上面標出了四個關鍵點:
not ready
狀態,不能開放使用。通過上面的步驟,咱們已經建立好了全部的布隆過濾器。
這一步主要是從數據庫中查詢數據,而後塞到布隆過濾器中。可能有朋友會問,爲何還要把初始化這一步單獨拎出來?直接在建立bean的時候初始化不就好了。主要是由於在動態註冊 bean 的時候,容器的上下文環境尚未準備好階段。在容器中還沒法獲取到其餘的 bean 。因此咱們只能把初始化放在這一階段來作。
在這裏做者使用了 ApplicationListener 監聽,他能夠監聽 SpringBoot 應用的生命週期,做者在這裏監聽了 SpringBoot 啓動完成以後的事件,把對布隆過濾器的初始化工做放到這裏面來進行。
DeduplicationApplicationListener 監聽器核心代碼:
initBloomFilter()
初始化每一個布隆過濾器。initBloomFilter 方法:
ApplicationContextAware
動態獲取 bean 實例。fillBloomFilter
方法。fillBloomFilter 方法:
這裏面主要的工做就是將數據庫中的數據塞到布隆過濾器中,一般這一步查詢出來的數據會很是的多,這裏選擇了將查詢任務切分,多線程查詢,而後再合併子任務,沒錯!就是借鑑 MapReduce 的思想。
數據塞完以後,該布隆過濾器就能夠對外開放使用了,因此會將該布隆過濾器對應的初始化狀態置爲 true。
通過上面的步驟,已經將布隆過濾器初始化完成了,下面對布隆過濾器進行必定的封裝就可使用了。
這一步主要是對布隆過濾器進行封裝,使之用起來更加的方便,這裏主要支持兩種類型的數據源去重,分別是 List 和 CSV 文件。這裏都是批量去重,入庫也是批量入庫。若是須要單條記錄去重,例如過濾黑名單,就須要本身封裝對應的業務了,由於畢竟不一樣用戶的業務仍是不太同樣的。
對外暴露的兩個方法:
go 是一個重載函數,僅對內部使用:
這裏再來看看 findNotExistRecordFromList 方法。很簡單,遍歷傳入的 list,若是布隆過濾器判斷爲還不存在,加入到結果集中返回。
關於去重器的核心步驟就介紹到這裏啦,對於實現的細節,感興趣的朋友能夠到 github 上把代碼 fork 下來看一哈,有問題歡迎隨時與我交流。
✨github:cranberry✨ 是做者最近在寫的一個項目,主要是對 java web 開發過程當中的一些常見問題的實現與分析,後續也會持續更新,但願對你們會有幫助,感受不錯也能夠給作個 star 一個哦!