1. 程式人生 > 實用技巧 >一致性hash演算法及java實現

一致性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演算法 
 * 重點:1.如何造一個hash環,2.如何在雜湊環上對映伺服器節點,3.如何找到對應的節點
 */  
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]) + "]");  
    }  
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72

執行結果:

[192.168.0.0:111]加入集合中, 其Hash值為575774686
[192.168.0.1:111]加入集合中, 其Hash值為8518713
[192.168.0.2:111]加入集合中, 其Hash值為1361847097
[192.168.0.3:111]加入集合中, 其Hash值為1171828661
[192.168.0.4:111]加入集合中, 其Hash值為1764547046

[太陽]hash值為1977106057, 被路由到結點[192.168.0.1:111]
[月亮]hash值為1132637661, 被路由到結點[192.168.0.3:111]
[星星]hash值為880019273, 被路由到結點[192.168.0.3:111]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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]) + "]");  
     }  
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94

執行結果:

虛擬節點[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.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]
[月亮]hash值為1132637661, 被路由到結點[192.168.0.4:111]
[星星]hash值為880019273, 被路由到結點[192.168.0.3:111]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

原文:
一致性hash演算法與java實現
每天進步一點點——五分鐘理解一致性雜湊演算法(consistent hashing)
對一致性Hash演算法,Java程式碼實現的深入研究