為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

crossoverJie發表於2019-01-19

前言

最近這段時間確實有點忙,這篇的目錄還是在飛機上敲出來了的。

言歸正傳,上週更新了 cim 第一版;沒想到反響熱烈,最高時上了 GitHub Trending Java 版塊的首位,一天收到了 300+ 的 star。

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

現在總共也有 1.3K+ 的 star,有幾十個朋友參加了測試,非常感謝大家的支援。

在這過程中也收到一些 bug 反饋,feature 建議;因此這段時間我把一些影響較大的 bug 以及需求比較迫切的 feature 調整了,本次更新的 v1.0.1 版本:

  • 客戶端超時自動下線。
  • 新增 AI 模式。
  • 聊天記錄查詢。
  • 線上使用者字首模糊匹配。

下面談下幾個比較重點的功能。

客戶端超時自動下線 這個功能涉及到客戶端和服務端的心跳設計,比較有意思,也踩了幾個坑;所以準備留到下次單獨來聊。

AI 模式

大家應該還記得這個之前刷爆朋友圈的 估值兩個一個億的 AI 核心程式碼

和我這裡的場景再合適不過了。

於是我新增了一個命令用於一鍵開啟 AI 模式,使用情況大概如下。

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

歡迎大家更新原始碼體驗,融資的請私聊我?。

聊天記錄

聊天記錄也是一個比較迫切的功能。

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

使用命令 :q 關鍵字 即可查詢與個人相關的聊天記錄。

這個功能其實比較簡單,只需要在訊息傳送及接收訊息時儲存即可。

但要考慮的一點是,這個儲存訊息是 IO 操作,不可避免的會有耗時;需要儘量避免對訊息傳送、接收產生影響。

非同步寫入訊息

因此我把訊息寫入的過程非同步完成,可以不影響真正的業務。

實現起來也挺簡單,就是一個典型的生產者消費者模式。

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

主執行緒收到訊息之後直接寫入佇列,另外再有一個執行緒一直源源不斷的從佇列中取出資料後儲存聊天記錄。

大概的程式碼如下:

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】
為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】


寫入訊息的同時會把消費訊息的執行緒開啟:

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

而最終存放訊息記錄的策略,考慮後還是以最簡單的方式存放在客戶端,可以降低複雜度。

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

簡單來說就是根據當前日期+使用者名稱寫入到磁碟裡。

當客戶端關閉時利用執行緒中斷的方式停止了消費佇列的執行緒。

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】
為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

這點的設計其實和 logback 寫日誌的方式比較類似,感興趣的可以去翻翻 logback 的原始碼,更加詳細。

回撥介面

至於收到其他客戶端發來的訊息時則是利用之前預留的訊息回撥介面來寫入日誌。

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

收到訊息後會執行自定義的回撥介面。

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

於是在這個回撥方法中實現寫入邏輯即可,當後續還有其他的訊息處理邏輯時也能在這裡直接新增。

當處理邏輯增多時最好是改為責任鏈模式,更加清晰易維護。

查詢演算法

接下來是本文著重要討論的一個查詢演算法,準確的說是一個字首模糊匹配的演算法。

實現的效果如下:

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

使用命令 :qu prefix 可以按照字首的方式搜尋使用者資訊。

當然在命令列中其實意義不大,但是在移動端中確是比較有用的。類似於微信按照使用者名稱匹配:

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

因為後期打算出一個移動端 APP,所以就先把這個功能實現了。

從效果也看得出來:就是按照輸入的字首匹配字串(目前只支援英文)。

在沒有任何限制的條件下最快、最簡單的實現方式可以直接把所有的字串存放在一個容器中 (List、Set),查詢時則挨個遍歷;利用 String.startsWith("prefix") 進行匹配。

但這樣會有幾個問題:

  • 儲存資源比較浪費,不管是 list 還是 Set 都會有額外的損耗。
  • 查詢效率較低,需要遍歷集合後再遍歷字串的 char 陣列(String.startsWith 的實現方式)。

字典樹

基於以上的問題我們可以考慮下:

假設我需要存放 java,javascript,jsp,php 這些字串時在 ArrayList 中會怎麼存放?

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

很明顯,會是這樣完整的存放在一個陣列中;同時這個陣列還可能存在浪費,沒有全部使用完。

但其實仔細觀察這些資料會發現有一些共同特點,比如 java,javascript 有共同的字首 java;和 jsp 有共同的字首 j

那是否可以把這些字首利用起來呢?這樣就可以少儲存一份。

比如寫入 java,javascript 這兩個字串時存放的結構如下:

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

當再存入一個 jsp 時:

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

最後再存入 jsf 時:

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

相信大家應該已經看明白了,按照這樣的儲存方式可以節省很多記憶體,同時查詢效率也比較高。

比如查詢以 jav 開頭的資料,只需要從頭結點 j 開始往下查詢,最後會查詢到 ava 以及 script 這兩個個結點,所以整個查詢路徑所經歷的字元拼起來就是查詢到的結果java+javascript

如果以 b 開頭進行查詢,那第一步就會直接返回,這樣比在 list 中的效率高很多。

但這個圖還不完善,因為不知道查詢到啥時候算是匹配到了一個之前寫入的字串。

比如在上圖中怎麼知道 j+ava 是一個我們之前寫入的 java 這個字元呢。

因此我們需要對這種是一個完整字串的資料打上一個標記:

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

比如這樣,我們將 ava、script、p、f 這幾個節點都換一個顏色表示。表明查詢到這個字元時就算是匹配到了一個結果。

而查到 s 這個字元顏色不對,代表還需要繼續往下查。

比如輸入關鍵字 js 進行匹配時,當它的查詢路徑走到 s 這裡時判斷到 s 的顏色不對,所以不會把 js 作為一個匹配結果。而是繼續往下查,發現有兩個子節點 p、f 顏色都正確,於是把查詢的路徑 jspjsf 都作為一個匹配結果。

而只輸入 j,則會把下面所有有色的字元拼起來作為結果集合。

這其實就一個典型的字典樹。

具體實現

下面則是具體的程式碼實現,其實演算法不像是實現一個業務功能這樣好用文字分析;具體還是看原始碼多除錯就明白了。

談下幾個重點的地方吧:

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

字典樹的節點實現,其中的 isEnd 相當於圖中的上色。

利用一個 Node[] children 來存放子節點。

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

為了可以區分大小寫查詢,所以子節點的長度相當於是 26*2

寫入資料

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

這裡以一個單測為例,寫入了三個字串,那最終形成的資料結構如下:

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

圖中有與上圖有幾點不同:

  • 每個節點都是一個字元,這樣樹的高度最高為52。
  • 每個節點的子節點都是長度為 52 的陣列;所以可以利用陣列的下標表示他代表的字元值。比如 0 就是大 A,26 則是小 a,以此類推。
  • 有點類似於之前提到的布隆過濾器,可以節省記憶體。

debug 時也能看出符合上圖的資料結構:

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

所以真正的寫入步驟如下:

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

  1. 把字串拆分為 char 陣列,並判斷大小寫計算它所存放在陣列中的位置 index
  2. 將當前節點的子節點陣列的 index 處新增一個節點。
  3. 如果是最後一個字元就將新增的節點置為最後一個節點,也就是上文的改變節點顏色。
  4. 最後將當前節點指向下一個節點方便繼續寫入。

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】
為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

查詢總的來說要麻煩一些,其實就是對樹進行深度遍歷;最終的思想看圖就能明白。

所以在 cim 中進行模糊匹配時就用到了這個結構。

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

字典樹的原始碼在此處:

github.com/crossoverJi…

其實利用這個結構還能實現判斷某個字首的單詞是否在某堆資料裡、某個字首的單詞出現的次數等。

總結

目前 cim 還在火熱內測中(雖然群裡只有20幾人),感興趣的朋友可以私聊我拉你入夥☺️

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

再沒有新的 BUG 產生前會著重把這些功能完成了,不出意外下週更新 cim 的心跳重連等機制。

完整原始碼:

github.com/crossoverJi…

如果這篇對你有所幫助還請不吝轉發。

為自己搭建一個分散式 IM 系統二【從查詢演算法聊起】

相關文章