Java之一致性hash算法原理及實現

一致性哈希算法是分佈式系統中經常使用的算法。html

好比,一個分佈式的存儲系統,要將數據存儲到具體的節點上,若是採用普通的hash方法,將數據映射到具體的節點上,如key%N,key是數據的key,N是機器節點數,若是有一個機器加入或退出這個集羣,則全部的數據映射都無效了。

一致性哈希算法,解決了普通餘數Hash算法伸縮性差的問題,能夠保證在上線、下線服務器的狀況下,儘可能有多的請求命中原來路由到的服務器。java

1、一致性哈希算法

一、原理

算法的具體原理這裏再次貼上:node

先構造一個長度爲232的整數環(這個環被稱爲一致性Hash環),根據節點名稱的Hash值(其分佈爲[0, 232-1])將服務器節點放置在這個Hash環上,而後根據數據的Key值計算獲得其Hash值(其分佈也爲[0, 232-1]),接着在Hash環上順時針查找距離這個Key值的Hash值最近的服務器節點,完成Key到服務器的映射查找。算法

(1)環形Hash空間

按照經常使用的hash算法來將對應的key哈希到一個具備2^32次方個桶的空間中,即0~(2^32)-1的數字空間中。apache

如今咱們能夠將這些數字頭尾相連,想象成一個閉合的環形。以下圖服務器

(2)把數據經過必定的hash算法處理後映射到環上

如今咱們將object一、object二、object三、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中,object二、object4存儲到了NODE3中。spa

在這樣的部署環境中,hash環是不會變動的,所以,經過算出對象的hash值就能快速的定位到對應的機器中,這樣就能找到對象真正的存儲位置了。

二、機器的刪除與添加
普通hash求餘算法最爲不妥的地方就是在有機器的添加或者刪除以後會形成大量的對象存儲位置失效。下面來分析一下一致性哈希算法是如何處理的。

(1)節點(機器)的刪除
以上面的分佈爲例,若是NODE2出現故障被刪除了,那麼按照順時針遷移的方法,object3將會被遷移到NODE3中,這樣僅僅是object3的映射位置發生了變化,其它的對象沒有任何的改動。以下圖:

(2)節點(機器)的添加
若是往集羣中添加一個新的節點NODE4,經過對應的哈希算法獲得KEY4,並映射到環中,以下圖:

經過按順時針遷移的規則,那麼object2被遷移到了NODE4中,其它對象還保持着原有的存儲位置。
經過對節點的添加和刪除的分析,一致性哈希算法在保持了單調性的同時,仍是數據的遷移達到了最小,這樣的算法對分佈式集羣來講是很是合適的,避免了大量數據遷移,減少了服務器的的壓力。

三、平衡性--虛擬節點

根據上面的圖解分析,一致性哈希算法知足了單調性和負載均衡的特性以及通常hash算法的分散性,但這還並不能當作其被普遍應用的起因,

由於還缺乏了平衡性。下面將分析一致性哈希算法是如何知足平衡性的。

hash算法是不保證平衡的,如上面只部署了NODE1和NODE3的狀況(NODE2被刪除的圖),object1存儲到了NODE1中,而object二、object三、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

2、一致性hash算法的Java實現

一、不帶虛擬節點的

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]加入集合中, 其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]

二、帶虛擬節點的

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]
[月亮]的hash值爲1132637661, 被路由到結點[192.168.0.4:111]
[星星]的hash值爲880019273, 被路由到結點[192.168.0.3:111]
 

原文:

http://blog.csdn.net/cywosp/article/details/23397179/    一致性哈希算法

http://www.open-open.com/lib/view/open1455374048042.html  一致性哈希算法的Java實現

相關文章
相關標籤/搜索