家喻戶曉的一致性 Hash 算法是解決數據分散佈局或者說分佈式環境下系統伸縮性差的優質解,本文旨在使用 Java 語言手動實現一套該算法。java
最簡單的一個應用場景即是緩存,當單機緩存量過大時須要分庫,而後根據相關信息進行 hash 取模運算到指定的機器上去,好比 index = hash(ip) % N。node
可是當增長或者減小節點的時候,因爲上述公式的 N 值是有變化的,因此絕大部分,甚至說全部的緩存都會失效,對於這種場景最直接的解決辦法即是使用一致性 hash 算法。算法
關於一致性 Hash 算法,簡單的抽象描述就是一個圓環,而後上面均勻佈局了 2^32 個節點,好比 [0,1,2,4,8…],而後將咱們的機器節點散列在這個圓環上,至於散列的規則,可使用 hash(ip) 或者 hash(域名)。數組
當尋找數據的時候,只須要將目標數據的key散列在這個環上,而後進行順時針讀取最近的一個機器節點上的數據便可。緩存
以下圖的簡單版本,假如總共有3個數據節點(A、B、C),當須要查找的數據的key經計算在A和B之間,則順時針找,便找到了節點B。服務器
最大的優勢是:仍是以上面的案例爲背景,當節點B宕了以後,順時針便找到了C,這樣,影響的範圍僅僅是A和B之間的數據,對於其餘的數據是不影響的。數據結構
可是在散列數據節點的時候,緊湊性會受 hash 算法的影響,好比A、B、C三個數據服務器,在 hash 計算後散列在 一、二、4三個節點上,這樣就會由於太密集而失去**平衡性。**好比此時咱們要查找的數據的key通過 hash 運算以後,大機率是出如今4和1之間的,即在C以後,那樣的話順時針查找便會找到A,那麼A服務器便承載了幾乎全部的負載,這就失去了該算法的意義。分佈式
此時虛擬節點便出現了,好比上述的三臺服務器各虛擬分裂出1各虛擬節點(A一、B一、C1),那麼這樣即可以在必定程度上解決一致性hash的平衡性問題。ide
簡單描述下思路:其實就是使用一個數組去存儲全部的節點信息,存完以後須要手動排序一下,由於是有序的,因此取的時候就從 index 爲0開始挨個對比節點的 hash 值,直到找到一個節點的 hash 值是比咱們的目標數據的 hash(key) 大便可,不然返回第一個節點的數據。函數
package com.jet.mini.utils;
import java.util.Arrays;
/** * @ClassName: SortArrayConsistentHash * @Description: 初代數組實現的一致性哈數算法 * @Author: Jet.Chen * @Date: 2019/3/19 23:11 * @Version: 1.0 **/
public class SortArrayConsistentHash {
/** * 最爲核心的數據結構 */
private Node[] buckets;
/** * 桶的初始大小 */
private static final int INITIAL_SIZE = 32;
/** * 當前桶的大小 */
private int length = INITIAL_SIZE;
/** * 當前桶的使用量 */
private int size = 0;
public SortArrayConsistentHash(){
buckets = new Node[INITIAL_SIZE];
}
/** * 指定數組長度的構造 */
public SortArrayConsistentHash(int length){
if (length < 32) {
buckets = new Node[INITIAL_SIZE];
} else {
this.length = length;
buckets = new Node[length];
}
}
/** * @Description: 寫入數據 * @Param: [hash, value] * @return: void * @Author: Jet.Chen * @Date: 2019/3/19 23:38 */
public void add(long hash, String value){
// 大小判斷是否須要擴容
if (size == length) reSize();
Node node = new Node(value, hash);
buckets[++size] = node;
}
/** * @Description: 刪除節點 * @Param: [hash] * @return: boolean * @Author: Jet.Chen * @Date: 2019/3/20 0:24 */
public boolean del(long hash) {
if (size == 0) return false;
Integer index = null;
for (int i = 0; i < length; i++) {
Node node = buckets[i];
if (node == null) continue;
if (node.hash == hash) index = i;
}
if (index != null) {
buckets[index] = null;
return true;
}
return false;
}
/** * @Description: 排序 * @Param: [] * @return: void * @Author: Jet.Chen * @Date: 2019/3/19 23:48 */
public void sort() {
// 此處的排序不須要關注 eqals 的狀況
Arrays.sort(buckets, 0, size, (o1, o2) -> o1.hash > o2.hash ? 1 : -1);
}
/** * @Description: 擴容 * @Param: [] * @return: void * @Author: Jet.Chen * @Date: 2019/3/19 23:42 */
public void reSize() {
// 擴容1.5倍
int newLength = length >> 1 + length;
buckets = Arrays.copyOf(buckets, newLength);
}
/** * @Description: 根據一致性hash算法獲取node值 * @Param: [hash] * @return: java.lang.String * @Author: Jet.Chen * @Date: 2019/3/20 0:16 */
public String getNodeValue(long hash) {
if (size == 0) return null;
for (Node bucket : buckets) {
// 防止空節點
if (bucket == null) continue;
if (bucket.hash >= hash) return bucket.value;
}
// 防止循環沒法尾部對接首部
// 場景:僅列出node的hash值,[null, 2, 3...],可是尋求的hash值是4,上面的第一遍循環很顯然沒能找到2這個節點,全部須要再循環一遍
for (Node bucket : buckets) {
if (bucket != null) return bucket.value;
}
return null;
}
/** * node 記錄了hash值和原始的IP地址 */
private class Node {
public String value;
public long hash;
public Node(String value, long hash) {
this.value = value;
this.hash = hash;
}
@Override
public String toString() {
return "Node{hash="+hash+", value="+value+"}";
}
}
}
複製代碼
① 排序算法:上面直接使用 Arrays.sort() ,即 TimSort 排序算法,這個值得改進;
② hash 算法:上文沒有說起 hash 算法,須要改進;
③ 數據結構:上文使用的是數組,可是須要手動進行排序,優勢是插入速度尚可,可是擴容不便,並且須要手動排序,排序的時機也不定,須要改進;
④ 虛擬節點:沒有考慮虛擬節點,須要改進。
上文的實現既然有弊端,那就操刀改進之:
① 數據結構:咱們可使用 TreeMap 數據結構,優勢是該數據結構是有序的,無需再排序,並且該數據結構中有個函數叫 tailMap,做用是獲取比指定的 key 大的數據集合;
② hash 算法:此處咱們使用 FNV1_32_HASH 算法,該算法證明下來散列分佈比較均勻,hash 碰撞尚且 ok;
③ 虛擬節點:咱們暫且設置每一個節點鎖裂變的虛擬節點數量爲10。
代碼也不難,也是直接擼:
package com.jet.mini.utils;
import java.util.SortedMap;
import java.util.TreeMap;
/** * @ClassName: TreeMapConsistentHash * @Description: treeMap 實現的進化版一致性hash * @Author: Jet.Chen * @Date: 2019/3/20 20:44 * @Version: 1.0 **/
public class TreeMapConsistentHash {
/** * 主要數據結構 */
private TreeMap<Long, String> treeMap = new TreeMap<>();
/** * 自定義虛擬節點數量 */
private static final int VIRTUAL_NODE_NUM = 10;
/** * 普通的增長節點 */
@Deprecated
public void add (String key, String value) {
long hash = hash(key);
treeMap.put(hash, value);
}
/** * 存在虛擬節點 */
public void add4VirtualNode(String key, String value) {
for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
long hash = hash(key + "&&VIR" + i);
treeMap.put(hash, value);
}
treeMap.put(hash(key), value);
}
/** * 讀取節點值 * @param key * @return */
public String getNode(String key) {
long hash = hash(key);
SortedMap<Long, String> sortedMap = treeMap.tailMap(hash);
String value;
if (!sortedMap.isEmpty()) {
value = sortedMap.get(sortedMap.firstKey());
} else {
value = treeMap.firstEntry().getValue();
}
return value;
}
/** * 使用的是 FNV1_32_HASH */
public long hash(String key) {
final int p = 16777619;
int hash = (int)2166136261L;
for(int i = 0; i < key.length(); i++) {
hash = (hash ^ key.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;
}
}
複製代碼
一、虛擬節點的數量建議:
看上圖,X 軸是虛擬節點數量,Y 軸是服務器數量,很顯然,服務器越多,建議的虛擬節點數量也就越少。