在做缓存集群时,为了缓解服务器的压力,会部署多台缓存服务器,把数据资源均匀的分配到每个服务器上,分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。
一致性哈希的目的就是为了在节点数目发生改变时尽可能少的迁移数据,将所有的存储节点排列在收尾相接的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。