Java一致性Hash算法的实现
哈希hash
hash的意思是散列,目的是將一組輸入的數據均勻的分開、打散,往往用來配合路由算法做負載均衡,多用在分布式系統中。比如memcached,它只提供了K V的存儲、讀取,如果使用了多臺memcache做一個“邏輯集群”,就需要客戶端做“路由算法”,來保證數據均勻的進去,然后能“原路”拿出來。
常規哈希取模
常規哈希,往往結合取模運算,以便將請求轉發到后端的服務器上,如下圖:
第一步使用hash算法,將請求“打散”得到一個整數(比如傳遞過來一個請求,使用jdk類庫的hash對某個參數做計算),第二步將得到的參數對后端的服務器臺數取模,以上圖為例,假設有三臺服務器,那么id分別為1~6的請求會被轉發到1,2,0,1,2,0,上,不管請求id數是多少,總是這么周而復始的轉發。
假設上面是個緩存系統,以上請求為set請求,在服務器數量不變的情況下,對同樣的id做get請求,由于采用同樣的hash算法,那么肯定能原路找到對應的key值。這個算法簡單,而且數據分散的比較均勻。
如果系統訪問量突增,為了擴容加了一臺機器,編號為3,此時有了4臺機器,采用同樣的算法再去get請求會如何?比如id=6,這個時候 6%4=2,我們知道set時值其實放進了索引為0的機器,這個時候就get不到了。這就是上面算法的弊端,在增減機器時會使舊的數據大量“失效”,也就是命中率下降。
不帶虛擬節點的一致性哈希算法
為了解決以上問題,聰明的人發明了一致性哈希算法。思路是這樣,hash算法出來的整數有個范圍,我們在這個范圍內布置三臺服務器(范圍具體是多少看前面的hash算法)。假設hash的范圍是1~300,每臺負責一段范圍內的請求,比如一臺負責(1~100],一臺負責(100~200],一臺負責(200~1]。這三臺server首尾相接覆蓋/閉環了所有請求,稱為哈希環,如下圖:
如何實現一臺服務器接收一個范圍的請求?這個時候不用取模了,而是將server也按照hash算法計算一個id值,比如按照他們的ip+port+name拼成的串計算,假設正好分別是 1,100,200,將他們放進一個treeMap里,Map<Integer,Node> ,其中Node代表server節點,是自定義的數據結構,比如是一個類,包含ip,port,name等屬性。我們的例子中,map里包含三個元素。
一個請求過來,hash得到的值必屬于這三個server的范圍,比如一個請求id=N,那么從map里get(N)去找server,找到直接轉發,找不到進行如下運算:treemap里有個關鍵的api,tailMap(),這個接口能夠返回id比N大的map的子集,然后取子集的第一個節點,就是id=100的節點,通常稱為順時針查找。
? ?
//得到應當路由到的結點(示例代碼用String代表的節點)private static String getServer(String key) {//得到該key的hash值int hash = getHash(key);//得到大于該Hash值的所有MapSortedMap<Integer, String> subMap = sortedMap.tailMap(hash);if(subMap.isEmpty()){//如果沒有比該key的hash值大的,則從第一個node開始Integer i = sortedMap.firstKey();//返回對應的服務器return sortedMap.get(i);}else{//第一個Key就是順時針過去離node最近的那個結點Integer i = subMap.firstKey();//返回對應的服務器return subMap.get(i);}}當然如果子集為空,這意味著N>200,就取整個map的第一個節點,完成閉環。
分析:從實現可以看出,如果一個節點掛了,他的流量會順時針(逆時針實現也是一樣的)“導流”到下一個節點,其他節點不受影響。假如有100臺服務器,一臺掛了,其他99臺都能正常命中!這個算法比簡單的取模好了很多。
不過這里仍有個問題,假設各臺服務器性能差不多,此時流量突增,一臺server由于流量過載而掛掉,那么它的下一臺因為承載了2倍的流量,很有可能也會掛掉,依此類推,最后所有的節點都會掛掉,造成“雪崩”!
因此正常情況下,我們往往采用帶虛擬節點的一致性哈希算法(不特別說明的一致性哈希算法一般都是指的帶虛擬節點的算法)。
帶虛擬節點的一致性哈希算法
帶虛擬節點的一致性哈希算法是為了解決不帶虛擬節點算法的雪崩問題,虛擬節點也稱為分片。在上一步的基礎上理解虛擬節點是非常容易的。“虛擬”節點是server的副本、分身,每個虛擬節點存儲的server信息還是后面的物理地址,只不過每個server由一臺變成了多臺,這個時候往treeMap放節點時往往這么做:
for(i=1 ?--> ?N) // N為每個server對應的分片數量 {Map.put(hash(ip+port+name+i),node) // 所有虛擬節點放進去 }這個for循環外面還會有個循環,處理所有server node
由于每個server的ip,name不同,所以以上拼串hash后的值碰撞的概率是很小的,這樣所有的虛擬節點也會離散的分布到環上,形成的hash環如下圖,同樣顏色的虛擬節點同屬于一個server。
這個時候如果紅顏色的server掛了,它的虛擬節點負責的范圍會分別導航到下一個虛擬節點上,這些虛擬節點分別屬于不同的server,就避免了流量全部導流到一臺機器上。由于流量被均攤了,有效的減少了雪崩發生的概率。(理論上仍存在虛擬節點后面的虛擬節點屬于同一個server的情況,但是當虛擬節點非常多時,這個概率是非常小的,而且這個分片數量是自定義的,往往設置幾百個)。
只要是hash算法,就有哈希碰撞的可能性,在增加server時,計算后的虛擬節點跟其他server的虛擬節點重復的話,也會導致部分緩存失效(可以通過算法改良)。
綜上,一致性哈希算法并不是強一致性,也不是高可用方案,如果server掛了數據丟了就是丟了,除非有恢復手段,它只是一種減少由擴縮容引起的命中率下降的手段。
代碼可參考如下鏈接
https://blog.csdn.net/WANGYAN9110/article/details/70185652
https://blog.csdn.net/u010558660/article/details/52767218
————————————————
版權聲明:本文為CSDN博主「飛出銀河系」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/flyfeifei66/article/details/82458618
總結
以上是生活随笔為你收集整理的Java一致性Hash算法的实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微服务为什么一定要Zookeeper?
- 下一篇: 常见的一些 Hash 函数