一致性hash算法及java實現


一致性hash算法是分布式中一個常用且好用的分片算法、或者數據庫分庫分表算法。現在的互聯網服務架構中,為避免單點故障、提升處理效率、橫向擴展等原因,分布式系統已經成為了居家旅行必備的部署模式,所以也產出了幾種數據分片的方法:
1.取模,2.划段,3.一致性hash
前兩種有很大的一個問題就是需要固定的節點數,即節點數不能變,不能某一個節點掛了或者實時增加一個節點,變了分片規則就需要改變,需要遷移的數據也多。
那么一致性hash是怎么解決這個問題的呢?
一致性hash:對節點和數據,都做一次hash運算,然后比較節點和數據的hash值,數據值和節點最相近的節點作為處理節點。為了分布得更均勻,通過使用虛擬節點的方式,每個節點計算出n個hash值,均勻地放在hash環上這樣數據就能比較均勻地分布到每個節點。
1、原理
(1)環形Hash空間
按照常用的hash算法來將對應的key哈希到一個具有2^32次方個桶的空間中,即0~(2^32)-1的數字空間中。
現在我們可以將這些數字頭尾相連,想象成一個閉合的環形。如下圖

 


(2)把數據通過一定的hash算法處理后映射到環上
現在我們將object1、object2、object3、object4四個對象通過特定的Hash函數計算出對應的key值,然后散列到Hash環上。如下圖:
Hash(object1) = key1;
Hash(object2) = key2;
Hash(object3) = key3;
Hash(object4) = key4;

 


(3)將機器通過hash算法映射到環上
在采用一致性哈希算法的分布式集群中將新的機器加入,其原理是通過使用與對象存儲一樣的Hash算法將機器也映射到環中
(一般情況下對機器的hash計算是采用機器的IP或者機器唯一的別名作為輸入值),然后以順時針的方向計算,將所有對象存儲到離自己最近的機器中。
假設現在有NODE1,NODE2,NODE3三台機器,通過Hash算法得到對應的KEY值,映射到環中,其示意圖如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;

 


通過上圖可以看出對象與機器處於同一哈希空間中,這樣按順時針轉動object1存儲到了NODE1中,object3存儲到了NODE2中,object2、object4存儲到了NODE3中。
在這樣的部署環境中,hash環是不會變更的,因此,通過算出對象的hash值就能快速的定位到對應的機器中,這樣就能找到對象真正的存儲位置了。
2、機器的刪除與添加
普通hash求余算法最為不妥的地方就是在有機器的添加或者刪除之后會造成大量的對象存儲位置失效。下面來分析一下一致性哈希算法是如何處理的。
(1)節點(機器)的刪除
以上面的分布為例,如果NODE2出現故障被刪除了,那么按照順時針遷移的方法,object3將會被遷移到NODE3中,這樣僅僅是object3的映射位置發生了變化,其它的對象沒有任何的改動。如下圖:

 


(2)節點(機器)的添加
如果往集群中添加一個新的節點NODE4,通過對應的哈希算法得到KEY4,並映射到環中,如下圖:

 


通過按順時針遷移的規則,那么object2被遷移到了NODE4中,其它對象還保持着原有的存儲位置。
通過對節點的添加和刪除的分析,一致性哈希算法在保持了單調性的同時,還是數據的遷移達到了最小,這樣的算法對分布式集群來說是非常合適的,避免了大量數據遷移,減小了服務器的的壓力。
3、平衡性–虛擬節點
根據上面的圖解分析,一致性哈希算法滿足了單調性和負載均衡的特性以及一般hash算法的分散性,但這還並不能當做其被廣泛應用的原由,
因為還缺少了平衡性。下面將分析一致性哈希算法是如何滿足平衡性的。
hash算法是不保證平衡的,如上面只部署了NODE1和NODE3的情況(NODE2被刪除的圖),object1存儲到了NODE1中,而object2、object3、object4都存儲到了NODE3中,這樣就造成了非常不平衡的狀態。在一致性哈希算法中,為了盡可能的滿足平衡性,其引入了虛擬節點。
——“虛擬節點”( virtual node )是實際節點(機器)在 hash 空間的復制品( replica ),一個實際節點(機器)對應了若干個“虛擬節點”,這個對應個數也成為“復制個數”,“虛擬節點”在 hash 空間中以hash值排列。
以上面只部署了NODE1和NODE3的情況(NODE2被刪除的圖)為例,之前的對象在機器上的分布很不均衡,現在我們以2個副本(復制個數)為例,這樣整個hash環中就存在了4個虛擬節點,最后對象映射的關系圖如下:

 


根據上圖可知對象的映射關系:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通過虛擬節點的引入,對象的分布就比較均衡了。那么在實際操作中,正真的對象查詢是如何工作的呢?對象從hash到虛擬節點到實際節點的轉換如下圖:

 


“虛擬節點”的hash計算可以采用對應節點的IP地址加數字后綴的方式。例如假設NODE1的IP地址為192.168.1.100。引入“虛擬節點”前,計算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虛擬節點”后,計算“虛擬節”點NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2

二、一致性hash算法的Java實現。
1、不帶虛擬節點的

package hash;
 
import java.util.SortedMap;
import java.util.TreeMap;
 
/**
 * 不帶虛擬節點的一致性Hash算法
 */
public class ConsistentHashingWithoutVirtualNode {
 
    //待添加入Hash環的服務器列表
    private static String[] servers = { "192.168.0.0:111", "192.168.0.1:111",
            "192.168.0.2:111", "192.168.0.3:111", "192.168.0.4:111" };
 
    //key表示服務器的hash值,value表示服務器
    private static SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>();
 
    //程序初始化,將所有的服務器放入sortedMap中
    static {
        for (int i=0; i<servers.length; i++) {
            int hash = getHash(servers[i]);
            System.out.println("[" + servers[i] + "]加入集合中, 其Hash值為" + hash);
            sortedMap.put(hash, servers[i]);
        }
        System.out.println();
    }
 
    //得到應當路由到的結點
    private static String getServer(String key) {
        //得到該key的hash值
        int hash = getHash(key);
        //得到大於該Hash值的所有Map
        SortedMap<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);
        }
    }
    
    //使用FNV1_32_HASH算法計算服務器的Hash值,這里不使用重寫hashCode的方法,最終效果沒區別
    private static int getHash(String str) {
        final int p = 16777619;
        int hash = (int) 2166136261L;
        for (int i = 0; i < str.length(); i++)
            hash = (hash ^ str.charAt(i)) * p;
        hash += hash << 13;
        hash ^= hash >> 7;
        hash += hash << 3;
        hash ^= hash >> 17;
        hash += hash << 5;
 
        // 如果算出來的值為負數則取其絕對值
        if (hash < 0)
            hash = Math.abs(hash);
        return hash;
        }
    public static void main(String[] args) {
String[] keys = {"太陽", "月亮", "星星","木星"};
for (int i = 0; i < keys.length; i++) {
System.out.println("[" + keys[i] + "]的hash值為" + getHash(keys[i])
+ ", 被路由到結點[" + getServer(keys[i]) + "]");
}
}
}
 

 

 

執行結果:

[192.168.0.0:111]join in collections, its hash code is 575774686
[192.168.0.1:111]join in collections, its hash code is 8518713
[192.168.0.2:111]join in collections, its hash code is 1361847097
[192.168.0.3:111]join in collections, its hash code is 1171828661
[192.168.0.4:111]join in collections, its hash code is 1764547046

[太陽]的hash值為1977106057, 被路由到結點[192.168.0.1:111]
[月亮]的hash值為1132637661, 被路由到結點[192.168.0.3:111]
[星星]的hash值為880019273, 被路由到結點[192.168.0.3:111]
[木星]的hash值為1574472932, 被路由到結點[192.168.0.4:111]


2、帶虛擬節點的

 

 

package hash;
 
import java.util.LinkedList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
 
import org.apache.commons.lang.StringUtils;
 
/**
  * 帶虛擬節點的一致性Hash算法
  */
 public class ConsistentHashingWithoutVirtualNode {
 
     //待添加入Hash環的服務器列表
     private static String[] servers = {"192.168.0.0:111", "192.168.0.1:111", "192.168.0.2:111",
             "192.168.0.3:111", "192.168.0.4:111"};
     
     //真實結點列表,考慮到服務器上線、下線的場景,即添加、刪除的場景會比較頻繁,這里使用LinkedList會更好
     private static List<String> realNodes = new LinkedList<String>();
     
     //虛擬節點,key表示虛擬節點的hash值,value表示虛擬節點的名稱
     private static SortedMap<Integer, String> virtualNodes = new TreeMap<Integer, String>();
             
     //虛擬節點的數目,這里寫死,為了演示需要,一個真實結點對應5個虛擬節點
     private static final int VIRTUAL_NODES = 5;
     
     static{
         //先把原始的服務器添加到真實結點列表中
         for(int i=0; i<servers.length; i++)
             realNodes.add(servers[i]);
         
         //再添加虛擬節點,遍歷LinkedList使用foreach循環效率會比較高
         for (String str : realNodes){
             for(int i=0; i<VIRTUAL_NODES; i++){
                 String virtualNodeName = str + "&&VN" + String.valueOf(i);
                 int hash = getHash(virtualNodeName);
                 System.out.println("虛擬節點[" + virtualNodeName + "]被添加, hash值為" + hash);
                 virtualNodes.put(hash, virtualNodeName);
             }
         }
         System.out.println();
     }
     
     //使用FNV1_32_HASH算法計算服務器的Hash值,這里不使用重寫hashCode的方法,最終效果沒區別
     private static int getHash(String str){
         final int p = 16777619;
         int hash = (int)2166136261L;
         for (int i = 0; i < str.length(); i++)
             hash = (hash ^ str.charAt(i)) * p;
         hash += hash << 13;
         hash ^= hash >> 7;
         hash += hash << 3;
         hash ^= hash >> 17;
         hash += hash << 5;
         
         // 如果算出來的值為負數則取其絕對值
         if (hash < 0)
             hash = Math.abs(hash);
         return hash;
     }
     
     //得到應當路由到的結點
     private static String getServer(String key){
        //得到該key的hash值
         int hash = getHash(key);
         // 得到大於該Hash值的所有Map
         SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);
         String virtualNode;
         if(subMap.isEmpty()){
            //如果沒有比該key的hash值大的,則從第一個node開始
            Integer i = virtualNodes.firstKey();
            //返回對應的服務器
            virtualNode = virtualNodes.get(i);
         }else{
            //第一個Key就是順時針過去離node最近的那個結點
            Integer i = subMap.firstKey();
            //返回對應的服務器
            virtualNode = subMap.get(i);
         }
         //virtualNode虛擬節點名稱要截取一下
         if(StringUtils.isNotBlank(virtualNode)){
             return virtualNode.substring(0, virtualNode.indexOf("&&"));
         }
         return null;
     }
    public static void main(String[] args) {
        String[] keys = {"太陽", "月亮", "星星","木星"};
        for (int i = 0; i < keys.length; i++) {
            System.out.println("[" + keys[i] + "]的hash值為" + getHash(keys[i])
                    + ", 被路由到結點[" + getServer(keys[i]) + "]");
        }
    }
}

 

  

 


執行結果:

虛擬節點[192.168.0.0:111&&VN0]被添加, hash值為1686427075
虛擬節點[192.168.0.0:111&&VN1]被添加, hash值為354859081
虛擬節點[192.168.0.0:111&&VN2]被添加, hash值為1306497370
虛擬節點[192.168.0.0:111&&VN3]被添加, hash值為817889914
虛擬節點[192.168.0.0:111&&VN4]被添加, hash值為396663629
虛擬節點[192.168.0.1:111&&VN0]被添加, hash值為1032739288
虛擬節點[192.168.0.1:111&&VN1]被添加, hash值為707592309
虛擬節點[192.168.0.1:111&&VN2]被添加, hash值為302114528
虛擬節點[192.168.0.1:111&&VN3]被添加, hash值為36526861
虛擬節點[192.168.0.1:111&&VN4]被添加, hash值為848442551
虛擬節點[192.168.0.2:111&&VN0]被添加, hash值為1452694222
虛擬節點[192.168.0.2:111&&VN1]被添加, hash值為2023612840
虛擬節點[192.168.0.2:111&&VN2]被添加, hash值為697907480
虛擬節點[192.168.0.2:111&&VN3]被添加, hash值為790847074
虛擬節點[192.168.0.2:111&&VN4]被添加, hash值為2010506136
虛擬節點[192.168.0.3:111&&VN0]被添加, hash值為891084251
虛擬節點[192.168.0.3:111&&VN1]被添加, hash值為1725031739
虛擬節點[192.168.0.3:111&&VN2]被添加, hash值為1127720370
虛擬節點[192.168.0.3:111&&VN3]被添加, hash值為676720500
虛擬節點[192.168.0.3:111&&VN4]被添加, hash值為2050578780
虛擬節點[192.168.0.4:111&&VN0]被添加, hash值為586921010
虛擬節點[192.168.0.4:111&&VN1]被添加, hash值為184078390
虛擬節點[192.168.0.4:111&&VN2]被添加, hash值為1331645117
虛擬節點[192.168.0.4:111&&VN3]被添加, hash值為918790803
虛擬節點[192.168.0.4:111&&VN4]被添加, hash值為1232193678
[太陽]的hash值為1977106057, 被路由到結點[192.168.0.2:111&&VN4]
[月亮]的hash值為1132637661, 被路由到結點[192.168.0.4:111&&VN4]
[星星]的hash值為880019273, 被路由到結點[192.168.0.3:111&&VN0]
[木星]的hash值為1574472932, 被路由到結點[192.168.0.0:111&&VN0]

 

---------------------
原文:https://blog.csdn.net/u011305680/article/details/79721030


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM