最近在工做中,因爲接手了JAVA相關的項目代碼,涉及到資源分配的相關知識,裏面用到了一致性哈希進行負載均衡,因而就趁這個機會學習了下一致性哈希的相關內容,在此作一下學習總結。node
一致性哈希的主要做用就是用來進行負載均衡,咱們設想這麼一個場景,咱們有四臺服務器,而後須要將一批資源均勻的分配到這四臺服務器上。算法
首先咱們先想象有這麼一個圓環(也叫哈希環),在哈希環上有2^32個節點;服務器
一、對服務器(能惟一標識該服務的屬性,好比服務器的IP)進行求哈希值,使其落到哈希環,如圖所示Node0-Node3(四個箭頭所示)對應四臺服務器;負載均衡
二、對咱們要分配的資源進行求哈希值,一樣落到哈希環上,如圖所示(圖中的星標標識落在哈希環上的資源);函數
三、規定落在哈希環上的資源,分配到該資源所在節點順時針遇到的第一個服務節點上,如圖所示;
工具
若是服務器所在的哈希環上的節點如上圖所示,均勻分佈,那天然是最好的,但若是像下圖所示,那麼就會致使資源的分配不均。學習
爲了解決這種狀況出現的問題,出現了虛擬節點的一致性哈希。spa
所謂的虛擬節點,就是咱們假想出來的節點,將這些虛擬節點映射到哈希環上,而後將這些虛擬節點與實際服務器進行關聯,例如虛擬節點Node0,Node1與服務器1關聯,那麼分配到Node0,Node1的資源,也便是分配到實際的服務器1上。code
也就是說咱們實際的服務器有四臺,但咱們能夠將N個虛擬的節點映射到哈希環上,這樣就能經過調節虛擬節點與實際服務器的關聯關係,來達到調節分配到不一樣服務器資源的數量的目的。server
/** * 計算哈希值的工具類 */ public class Hash { /** * 使用String自帶的hashCode函數 * @param str * @return */ public static Integer stringHashcode(String str){ int hash = str.hashCode(); if (hash<0){ hash = Math.abs(hash); } return hash; } /** * 使用FNV1_32_HASH算法計算hash值 * @param str * @return */ public static Integer FNV1_32_HASH(String str){ final int p = 1677769; int hash = (int)0; int len = str.length(); for (int i=0;i<len;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 class HashTest { private static String[] servers = {"192.168.0.1:8999","192.168.0.2:8999","192.168.0.0.3:8999"}; /** * key 服務器的哈希值、 * value 服務器信息 */ private static SortedMap<Integer,String> map = new TreeMap<Integer, String>(); static { /** * 將服務器信息存放到map中 */ int size = servers.length; for (int i = 0;i<size;i++){ int hash = Hash.FNV1_32_HASH(servers[i]); map.put(hash, servers[i]); System.out.println("hash="+hash+" "+"server="+servers[i]); } } /** * 獲取資源所在的節點服務器 * @param node * @return */ public static String getServer(String node){ Integer hash = Hash.FNV1_32_HASH(node); //獲取大於當前node的hash值的全部服務節點 SortedMap<Integer, String> sortedMap = map.tailMap(hash); Integer key = 0; if (sortedMap.isEmpty()){ //若是沒有比node節點hash值大的服務節點,則取hash值最小的服務節點 key = map.firstKey(); }else{ //獲取過濾出的服務節點的第一個值,也就是從node開始順時針遇到的第一個服務節點 key = sortedMap.firstKey(); } String server = map.get(key); return server; } public static void main(String[] args) { String[] index = {"582639584","582639585","582639586","582639587"}; System.out.println("-------------"); int size = index.length; for(int i=0;i<size;i++){ String server = getServer(index[i]); System.out.println("index="+index[i] +" hash="+Hash.FNV1_32_HASH(index[i]) +" server="+server); } } }
運行結果:
hash=474459784 server=192.168.0.1:8999 hash=1451861892 server=192.168.0.2:8999 hash=1078738565 server=192.168.0.0.3:8999 ------------- index=582639584 hash=1309097570 server=192.168.0.2:8999 index=582639585 hash=583335179 server=192.168.0.0.3:8999 index=582639586 hash=1918263339 server=192.168.0.1:8999 index=582639587 hash=1873142797 server=192.168.0.1:8999
/** * 帶虛擬節點的代碼實現 */ public class HashVirtualNodeTest { /** * 實際的服務器 */ private static String[] servers = {"192.168.0.1:8999","192.168.0.2:8999","192.168.0.0.3:8999"}; /** *每一個服務節點對應的虛擬服務節點數目 */ private final static int virtualNode = 5; /** * 哈希環上的虛擬服務節點 * key 虛擬服務節點的hash值 * value 虛擬服務節點名稱 */ private static SortedMap<Integer,String> virtualNodeMap = new TreeMap<Integer, String>(); static{ /** * 將虛擬節點放到virtualNodeMap中 * 虛擬節點與實際的服務器的對應關係 * 服務器名稱:server * 虛擬節點名:server&&Node0 server&&Node1 server&&Node2 ... */ int len = servers.length; for (int i=0;i<len;i++){ for(int j=0;j<virtualNode;j++){ String virtualNodeName = servers[i]+"&&"+"Node"+j; Integer hash = Hash.FNV1_32_HASH(virtualNodeName); virtualNodeMap.put(hash,virtualNodeName); System.out.println("hash="+hash+" virtualNode=" +virtualNodeName); } } } /** * 獲取一個節點對應的分配到的服務節點 * @param node 待查詢的節點 * @return 實際的服務節點名稱 */ public static String getServer(String node){ Integer hash = Hash.FNV1_32_HASH(node); //獲取大於當前節點的虛擬節點服務器 SortedMap<Integer, String> tailMap = virtualNodeMap.tailMap(hash); Integer key = 0; if(virtualNodeMap.isEmpty()){ //沒有大於當前節點的虛擬服務節點時,取虛擬服務節點的第一個 key = virtualNodeMap.firstKey(); }else{ //取出大於當前節點的第一個虛擬服務節點 key = tailMap.firstKey(); } //取出對應的虛擬節點 String virtualNode = virtualNodeMap.get(key); //獲取實際的服務名稱 String server = virtualNode.split("&&")[0]; return server; } public static void main(String[] args){ String[] index = {"582639584","582639585","582639586","582639587","582639588"}; System.out.println("------------------"); int size = index.length; for (int i=0;i<size;i++){ String server = getServer(index[i]); System.out.println("index="+index[i]+" hash="+ Hash.FNV1_32_HASH(index[i]) +" server="+server); } } }
運行結果:
hash=350621948 virtualNode=192.168.0.1:8999&&Node0 hash=2008701201 virtualNode=192.168.0.1:8999&&Node1 hash=399766123 virtualNode=192.168.0.1:8999&&Node2 hash=9572053 virtualNode=192.168.0.1:8999&&Node3 hash=310503051 virtualNode=192.168.0.1:8999&&Node4 hash=102010718 virtualNode=192.168.0.2:8999&&Node0 hash=1966288165 virtualNode=192.168.0.2:8999&&Node1 hash=1935085864 virtualNode=192.168.0.2:8999&&Node2 hash=2058939177 virtualNode=192.168.0.2:8999&&Node3 hash=1101158742 virtualNode=192.168.0.2:8999&&Node4 hash=1042841091 virtualNode=192.168.0.0.3:8999&&Node0 hash=1013102794 virtualNode=192.168.0.0.3:8999&&Node1 hash=1097523199 virtualNode=192.168.0.0.3:8999&&Node2 hash=384605107 virtualNode=192.168.0.0.3:8999&&Node3 hash=1615270737 virtualNode=192.168.0.0.3:8999&&Node4 ------------------ index=582639584 hash=1309097570 server=192.168.0.0.3:8999 index=582639585 hash=583335179 server=192.168.0.0.3:8999 index=582639586 hash=1918263339 server=192.168.0.2:8999 index=582639587 hash=1873142797 server=192.168.0.2:8999 index=582639588 hash=1040353541 server=192.168.0.0.3:8999