一致性哈希算法與Java實現
========================================================
一致性哈希算法是分佈式系統中常用的算法。比如,一個分佈式的存儲系統,要將數據存儲到具體的節點上,
如果採用普通的hash方法,將數據映射到具體的節點上,如key%N,key是數據的key,N是機器節點數,
如果有一個機器加入或退出這個集羣,則所有的數據映射都無效了。
一致性哈希算法解決了普通餘數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]加入集合中, 其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]
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]
[月亮]的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實現