一致性Hash演算法實現
阿新 • • 發佈:2020-12-30
技術標籤:Java進階知識
在做快取叢集時,為了緩解伺服器的壓力,會部署多臺快取伺服器,把資料資源均勻的分配到每個伺服器上,分散式資料庫首先要解決把整個資料集按照分割槽規則對映到多個節點的問題,即把資料集劃分到多個節點上,每個節點負責整體資料的一個子集。
一致性雜湊的目的就是為了在節點數目發生改變時儘可能少的遷移資料,將所有的儲存節點排列在收尾相接的Hash環上,每個key在計算Hash 後會順時針找到臨接的儲存節點存放。而當有節點加入或退 時,僅影響該節點在Hash環上順時針相鄰的後續節點。
常用一致性Hash演算法:CRC32_HASH、FNV1_32_HASH、KETAMA_HASH等。其中 KETAMA_HASH 是 MemeCache 推薦的一致性Hash演算法
Java程式碼實現:
import org.springframework.util.StringUtils;
import java.util.*;
/**
* 一致性hash演算法
* Hash演算法選擇:hashCode(),不夠雜湊(捨棄),選擇其他Hash演算法,如 CRC32_HASH、FNV1_32_HASH、KETAMA_HASH 等
* 其中 KETAMA_HASH 是MemeCache推薦的一致性Hash演算法
*/
public class ConsistencyHash {
// 儲存伺服器節點資訊
private static final List<String> SERVER_NODES = new ArrayList<>();
// 每個節點生成虛擬節點個數
private static final Integer VIRTUAL_NODE_NUM = 100;
// 虛擬節點與伺服器對應關係:以虛擬節點為key,伺服器為value,通過虛擬節點快速定位到伺服器位置
private static final TreeMap<Integer, String> VIRTUAL_SERVER_RELATION = new TreeMap<>();
/**
* 新增伺服器節點
* 1.將伺服器新增到 SERVER_NODES
* 2.為伺服器分配虛擬節點,並設定對應關係
* @param node
*/
public synchronized void addServer(String node) {
if (StringUtils.isEmpty(node)) {// 節點為空不新增
return;
} else if (SERVER_NODES.contains(node)) {// 節點以及存在不新增
return;
}
int count = 1;
// 1.將伺服器新增到 SERVER_NODES
SERVER_NODES.add(node);
// 2.分配虛擬節點,並設定對應關係
while (count <= VIRTUAL_NODE_NUM) {
int virtualHashValue = FNV1_32_HASH.getHash(node + "_virtual_node_" + count);
if (VIRTUAL_SERVER_RELATION.containsKey(virtualHashValue)) {// 如果已經該虛擬節點已經存在,則重新生成
continue;
}
VIRTUAL_SERVER_RELATION.put(virtualHashValue, node);
count++;
}
}
/**
* 移除服務節點
* 1.從 SERVER_NODES 移除伺服器
* 2.移除所有虛擬節點
* @param node
*/
public synchronized void removeServer(String node) {
// 1.從 SERVER_NODES 移除伺服器
if (SERVER_NODES.remove(node)) {
// 2.移除所有虛擬節點
Iterator<Map.Entry<Integer, String>> iterator = VIRTUAL_SERVER_RELATION.entrySet().iterator();
while (iterator.hasNext()) {
if (node.equals(iterator.next().getValue())){
iterator.remove();
}
}
}
}
/**
* 根據key獲取所處伺服器節點
* @param key
* @return
*/
public String getServer(String key) {
int hashValue = FNV1_32_HASH.getHash(key);
Map.Entry<Integer, String> virtualNode = VIRTUAL_SERVER_RELATION.ceilingEntry(hashValue);
if (virtualNode == null) {// 如果為空,則為第一個虛擬節點
return VIRTUAL_SERVER_RELATION.firstEntry().getValue();
}
return virtualNode.getValue();
}
private ConsistencyHash() {
}
/**
* IoDH 單例模式
*/
private static class IoDHSingleton {
private static final ConsistencyHash instance = new ConsistencyHash();
}
public static ConsistencyHash getInstance() {
return IoDHSingleton.instance;
}
/**
* 測試
* @param args
*/
public static void main(String[] args) {
ConsistencyHash consistencyHash = ConsistencyHash.getInstance();
// 初始化六個伺服器
List<String> servers = new ArrayList<>();
servers.add("10.11.12.1");
servers.add("10.11.12.2");
servers.add("10.11.12.3");
servers.add("10.11.12.4");
servers.add("10.11.12.5");
servers.add("10.11.12.6");
for (int i = 0, size = servers.size(); i < size; i++) {
consistencyHash.addServer(servers.get(i));
}
// 生成 10000 條資料,並存儲當前資料所在節點
int valNum = 10000;
Map<String, String> vals = new HashMap<>();
String val;
String server;
// 記錄伺服器節點個數
Map<String, Integer> countServerVal = new HashMap<>();
for (int i = 1; i <= valNum; i++) {
val = "node-" + i;
server = consistencyHash.getServer(val);
vals.put(val, server);
countServerVal.computeIfAbsent(server, k -> 0);
countServerVal.computeIfPresent(server, (k, v) -> v + 1);
}
System.out.println("當前伺服器分配資料情況:" + countServerVal);
// 計算新增一個節點後,資料匹配率:以此判斷當前資料所在節點和通過一致性hash演算法獲取伺服器節點對比
consistencyHash.addServer("10.11.12.7");
Iterator<Map.Entry<String, String>> iterator = vals.entrySet().iterator();
Map.Entry<String, String> entry;
// 記錄伺服器節點匹配個數
Integer countEquals = 0;
while (iterator.hasNext()) {
entry = iterator.next();
if (entry.getValue().equals(consistencyHash.getServer(entry.getKey()))) {
countEquals++;
}
}
System.out.println("當前伺服器資料匹配率:" + countEquals / Double.valueOf(valNum));
// TODO 刪除一個節點資料匹配率
}
}
執行結果:
採用一致性Hash演算法虛擬分割槽方式,當加入一臺伺服器後,資料匹配度大約為為 n / (n + 1),當刪除一臺伺服器後,資料匹配度大約為為( n - 1) / n。