爲本身搭建一個分佈式 IM 系統二【從查找算法聊起】

前言

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

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

如今總共也有 1.3K+ 的 star,有幾十個朋友參加了測試,很是感謝你們的支持。java

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

  • 客戶端超時自動下線。
  • 新增 AI 模式。
  • 聊天記錄查詢。
  • 在線用戶前綴模糊匹配。

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

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

AI 模式

你們應該還記得這個以前刷爆朋友圈的 估值兩個一個億的 AI 核心代碼數組

和我這裏的場景再合適不過了。微信

因而我新增了一個命令用於一鍵開啓 AI 模式,使用狀況大概以下。數據結構

歡迎你們更新源碼體驗,融資的請私聊我🤣。異步

聊天記錄

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

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

這個功能其實比較簡單,只須要在消息發送及接收消息時保存便可。

但要考慮的一點是,這個保存消息是 IO 操做,不可避免的會有耗時;須要儘可能避免對消息發送、接收產生影響。

異步寫入消息

所以我把消息寫入的過程異步完成,能夠不影響真正的業務。

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

主線程收到消息以後直接寫入隊列,另外再有一個線程一直源源不斷的從隊列中取出數據後保存聊天記錄。

大概的代碼以下:


寫入消息的同時會把消費消息的線程打開:

而最終存放消息記錄的策略,考慮後仍是以最簡單的方式存放在客戶端,能夠下降複雜度。

簡單來講就是根據當前日期+用戶名寫入到磁盤裏。

當客戶端關閉時利用線程中斷的方式中止了消費隊列的線程。

這點的設計其實和 logback 寫日誌的方式比較相似,感興趣的能夠去翻翻 logback 的源碼,更加詳細。

回調接口

至於收到其餘客戶端發來的消息時則是利用以前預留的消息回調接口來寫入日誌。

收到消息後會執行自定義的回調接口。

因而在這個回調方法中實現寫入邏輯便可,當後續還有其餘的消息處理邏輯時也能在這裏直接添加。

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

查找算法

接下來是本文着重要討論的一個查找算法,準確的說是一個前綴模糊匹配的算法。

實現的效果以下:

使用命令 :qu prefix 能夠按照前綴的方式搜索用戶信息。

固然在命令行中其實意義不大,可是在移動端中確是比較有用的。相似於微信按照用戶名匹配:

由於後期打算出一個移動端 APP,因此就先把這個功能實現了。

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

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

但這樣會有幾個問題:

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

字典樹

基於以上的問題咱們能夠考慮下:

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

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

但其實仔細觀察這些數據會發現有一些共同特色,好比 java,javascript 有共同的前綴 java;和 jsp 有共同的前綴 j

那是否能夠把這些前綴利用起來呢?這樣就能夠少存儲一份。

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

當再存入一個 jsp 時:

最後再存入 jsf 時:

相信你們應該已經看明白了,按照這樣的存儲方式能夠節省不少內存,同時查詢效率也比較高。

好比查詢以 jav 開頭的數據,只須要從頭結點 j 開始往下查詢,最後會查詢到 ava 以及 script 這兩個個結點,因此整個查詢路徑所經歷的字符拼起來就是查詢到的結果java+javascript

若是以 b 開頭進行查詢,那第一步就會直接返回,這樣比在 list 中的效率高不少。

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

好比在上圖中怎麼知道 j+ava 是一個咱們以前寫入的 java 這個字符呢。

所以咱們須要對這種是一個完整字符串的數據打上一個標記:

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

而查到 s 這個字符顏色不對,表明還須要繼續往下查。

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

而只輸入 j,則會把下面全部有色的字符拼起來做爲結果集合。

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

具體實現

下面則是具體的代碼實現,其實算法不像是實現一個業務功能這樣好用文字分析;具體仍是看源碼多調試就明白了。

談下幾個重點的地方吧:

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

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

爲了能夠區分大小寫查詢,因此子節點的長度至關因而 26*2

寫入數據

這裏以一個單測爲例,寫入了三個字符串,那最終造成的數據結構以下:

圖中有與上圖有幾點不一樣:

  • 每一個節點都是一個字符,這樣樹的高度最高爲52。
  • 每一個節點的子節點都是長度爲 52 的數組;因此能夠利用數組的下標表示他表明的字符值。好比 0 就是大 A,26 則是小 a,以此類推。
  • 有點相似於以前提到的布隆過濾器,能夠節省內存。

debug 時也能看出符合上圖的數據結構:

因此真正的寫入步驟以下:

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

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

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

字典樹的源碼在此處:

github.com/crossoverJi…

其實利用這個結構還能實現判斷某個前綴的單詞是否在某堆數據裏、某個前綴的單詞出現的次數等。

總結

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

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

完整源碼:

github.com/crossoverJi…

若是這篇對你有所幫助還請不吝轉發。

相關文章
相關標籤/搜索