記得一年前分享過一篇《一致性 Hash 算法分析》,當時只是分析了這個算法的實現原理、解決了什麼問題等。java
但沒有實際實現一個這樣的算法,畢竟要加深印象還得本身擼一遍,因而本次就當前的一個路由需求來着手實現一次。git
看過《爲本身搭建一個分佈式 IM(即時通信) 系統》的朋友應該對其中的登陸邏輯有所印象。github
先給新來的朋友簡單介紹下 cim 是幹啥的:
其中有一個場景是在客戶端登陸成功後須要從可用的服務端列表中選擇一臺服務節點返回給客戶端使用。面試
而這個選擇的過程就是一個負載策略的過程;初版本作的比較簡單,默認只支持輪詢的方式。算法
雖然夠用,但不夠優雅😏。設計模式
所以個人規劃是內置多種路由策略供使用者根據本身的場景選擇,同時提供簡單的 API 供用戶自定義本身的路由策略。數組
先來看看一致性 Hash 算法的一些特色:服務器
0 ~ 2^32-1
大小的環。根據這些客觀條件咱們很容易想到經過自定義一個有序數組來模擬這個環。數據結構
這樣咱們的流程以下:分佈式
先不考慮排序所消耗的時間,單看這個路由的時間複雜度:
O(1)
。O(N)
。理論講完了來看看具體實踐。
我自定義了一個類:SortArrayMap
他的使用方法及結果以下:
可見最終會按照 key
的大小進行排序,同時傳入 hashcode = 101
時會按照順時針找到 hashcode = 1000
這個節點進行返回。
下面來看看具體的實現。
成員變量和構造函數以下:
其中最核心的就是一個 Node
數組,用它來存放服務節點的 hashcode
以及 value
值。
其中的內部類 Node
結構以下:
寫入數據的方法以下:
相信看過 ArrayList
的源碼應該有印象,這裏的寫入邏輯和它很像。
可是存放時是按照寫入順序存放的,遍歷時天然不會有序;所以提供了一個 Sort
方法,能夠把其中的數據按照 key
其實也就是 hashcode
進行排序。
排序也比較簡單,使用了 Arrays
這個數組工具進行排序,它實際上是使用了一個 TimSort
的排序算法,效率仍是比較高的。
最後則須要按照一致性 Hash 的標準順時針查找對應的節點:
代碼仍是比較簡單清晰的;遍歷數組若是找到比當前 key 大的就返回,沒有查到就取第一個。
這樣就基本實現了一致性 Hash 的要求。
ps:這裏並不包含具體的 hash 方法以及虛擬節點等功能(具體實現請看下文),這個能夠由使用者來定,SortArrayMap 可做爲一個底層的數據結構,提供有序 Map 的能力,使用場景也不侷限於一致性 Hash 算法中。
SortArrayMap
雖然說是實現了一致性 hash 的功能,但效率還不夠高,主要體如今 sort
排序處。
下圖是目前主流排序算法的時間複雜度:
最好的也就是 O(N)
了。
這裏徹底能夠換一個思路,不用對數據進行排序;而是在寫入的時候就排好順序,只是這樣會下降寫入的效率。
好比二叉查找樹,這樣的數據結構 jdk
裏有現成的實現;好比 TreeMap
就是使用紅黑樹來實現的,默認狀況下它會對 key 進行天然排序。
來看看使用 TreeMap
如何來達到一樣的效果。
運行結果:
127.0.0.1000
效果和上文使用 SortArrayMap
是一致的。
只使用了 TreeMap 的一些 API:
TreeMap
能夠保證 key 的天然排序。tailMap
能夠獲取比當前 key 大的部分數據。Map
的第一個節點,一樣也實現了環形結構。ps:這裏一樣也沒有 hash 方法以及虛擬節點(具體實現請看下文),由於 TreeMap 和 SortArrayMap 同樣都是做爲基礎數據結構來使用的。
爲了方便你們選擇哪個數據結構,我用 TreeMap
和 SortArrayMap
分別寫入了一百萬條數據來對比。
先是 SortArrayMap
:
耗時 2237 毫秒。
TreeMap:
耗時 1316毫秒。
結果是快了將近一倍,因此仍是推薦使用 TreeMap
來進行實現,畢竟它不須要額外的排序損耗。
下面來看看在 cim
這個應用中是如何具體使用的,其中也包括上文提到的虛擬節點以及 hash 算法。
在應用的時候考慮到就算是一致性 hash 算法都有多種實現,爲了方便其使用者擴展本身的一致性 hash 算法所以我定義了一個抽象類;其中定義了一些模板方法,這樣你們只須要在子類中進行不一樣的實現便可完成本身的算法。
AbstractConsistentHash,這個抽象類的主要方法以下:
add
方法天然是寫入數據的。sort
方法用於排序,但子類也不必定須要重寫,好比 TreeMap
這樣自帶排序的容器就不用。getFirstNodeValue
獲取節點。process
則是面向客戶端的,最終只須要調用這個方法便可返回一個節點。下面咱們來看看利用 SortArrayMap
以及 AbstractConsistentHash
是如何實現的。
就是實現了幾個抽象方法,邏輯和上文是同樣的,只是抽取到了不一樣的方法中。
只是在 add 方法中新增了幾個虛擬節點,相信你們也看得明白。
把虛擬節點的控制放到子類而沒有放到抽象類中也是爲了靈活性考慮,可能不一樣的實現對虛擬節點的數量要求也不同,因此不如自定義的好。
可是 hash
方法確是放到了抽象類中,子類不用重寫;由於這是一個基本功能,只須要有一個公共算法能夠保證他散列地足夠均勻便可。
所以在 AbstractConsistentHash
中定義了 hash 方法。
這裏的算法摘抄自 xxl_job,網上也有其餘不一樣的實現,好比
FNV1_32_HASH
等;實現不一樣可是目的都同樣。
這樣對於使用者來講就很是簡單了:
他只須要構建一個服務列表,而後把當前的客戶端信息傳入 process
方法中便可得到一個一致性 hash 算法的返回。
一樣的對於想經過 TreeMap
來實現也是同樣的套路:
他這裏不須要重寫 sort 方法,由於自身寫入時已經排好序了。
而在使用時對於客戶端來講只需求修改一個實現類,其餘的啥都不用改就能夠了。
運行的效果也是同樣的。
這樣你們想自定義本身的算法時只須要繼承 AbstractConsistentHash
重寫相關方法便可,客戶端代碼無須改動。
但其實對於 cim
來講真正的擴展性是對路由算法來講的,好比它須要支持輪詢、hash、一致性hash、隨機、LRU等。
只是一致性 hash 也有多種實現,他們的關係就以下圖:
應用還須要知足對這一類路由策略的靈活支持,好比我也想自定義一個隨機的策略。
所以定義了一個接口:RouteHandle
public interface RouteHandle { /** * 再一批服務器裏進行路由 * @param values * @param key * @return */ String routeServer(List<String> values,String key) ; }
其中只有一個方法,也就是路由方法;入參分別是服務列表以及客戶端信息便可。
而對於一致性 hash 算法來講也是隻須要實現這個接口,同時在這個接口中選擇使用 SortArrayMapConsistentHash
仍是 TreeMapConsistentHash
便可。
這裏還有一個 setHash
的方法,入參是 AbstractConsistentHash;這就是用於客戶端指定須要使用具體的那種數據結構。
而對於以前就存在的輪詢策略來講也是一樣的實現 RouteHandle
接口。
這裏我只是把以前的代碼搬過來了而已。
接下來看看客戶端究竟是如何使用以及如何選擇使用哪一種算法。
爲了使客戶端代碼幾乎不動,我將這個選擇的過程放入了配置文件。
RouteHandle
接口的輪詢策略的全限定名。RouteHandle
接口的一致性 hash 算法的全限定名。SortArrayMapConsistentHash
仍是 TreeMapConsistentHash
或是自定義的其餘方案。AbstractConsistentHash
的全限定名。無論這裏的策略如何改變,在使用處依然保持不變。
只須要注入 RouteHandle
,調用它的 routeServer
方法。
@Autowired private RouteHandle routeHandle ; String server = routeHandle.routeServer(serverCache.getAll(),String.valueOf(loginReqVO.getUserId()));
既然使用了注入,那其實這個策略切換的過程就在建立 RouteHandle bean
的時候完成的。
也比較簡單,須要讀取以前的配置文件來動態生成具體的實現類,主要是利用反射完成的。
這樣處理以後就比較靈活了,好比想新建一個隨機的路由策略也是一樣的套路;到時候只須要修改配置便可。
感興趣的朋友也可提交 PR 來新增更多的路由策略。
但願看到這裏的朋友能對這個算法有所理解,同時對一些設計模式在實際的使用也能有所幫助。
相信在金三銀四的面試過程當中仍是能讓面試官眼前一亮的,畢竟根據我這段時間的面試過程來看聽過這個名詞的都在少數😂(可能也是和候選人都在 1~3 年這個層級有關)。
以上全部源碼:
https://github.com/crossoverJie/cim
若是本文對你有所幫助還請不吝轉發。