java 实现一致性 hash 算法

用户头像
李广富
关注
发布于: 2020 年 07 月 08 日

要求:使用java实现一致性 hash 算法,并编写测试用例测试这个算法,测试 100 万 KV 数据,10 个服务器节点的情况下,计算这些 KV 数据在服务器上分布数量的标准差,以评估算法的存储负载不均衡性。



1、定义hash抽象类,用于抽象hash的不同实现方法。常见的hash实现方式有三种,如下:

  • 重写String的hashCode(),但是它在一致性Hash环上分布不好;

  • KETAMA_HASH算法;

  • FNV1_32_HASH算法

第三种FNV1_32_HASH算法效率高些,这里采用该算法。

抽象类如下:

/**
* 抽象hash实现接口
*/
public interface HashFunction {
Integer hash(String key);
}



2、算法实现类,这里的算法为FNV1_32_HASH

public class HashFunctionImpl implements HashFunction {
@Override
public Integer 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;
}
}



3、定义服务器节点属性类,用于封装服务器的基本信息

/**
* 物理机节点模拟类,保存节点的IP、名称、端口等信息
*/
@Data
public class Node {
private String ip;
private String name;
public Node(String ip, String name) {
this.ip = ip;
this.name = name;
}
}



4、带虚拟节点的一致性算法

package com.li.hash.sevice;
import com.li.hash.entity.Node;
import java.util.Collection;
import java.util.SortedMap;
import java.util.TreeMap;
public class ConsistentHash {
private final HashFunction hashFunction;
/**
* 每个机器节点关联的虚拟节点个数
*/
private final int numberOfReplicas;
/**
* 环形虚拟节点
*/
private final SortedMap<Integer, Node> circle = new TreeMap<>();
/**
* @param hashFunction hash 函数接口
* @param numberOfReplicas 每个机器节点关联的虚拟节点个数
* @param nodes 真实机器节点
*/
public ConsistentHash(HashFunction hashFunction, int numberOfReplicas, Collection<Node> nodes) {
this.hashFunction = hashFunction;
this.numberOfReplicas = numberOfReplicas;
for (Node node : nodes) {
add(node);
}
}
/**
* 增加真实机器节点
*
* @param node
*/
public void add(Node node) {
for (int i = 0; i < this.numberOfReplicas; i++) {
circle.put(this.hashFunction.hash(node.getIp() + i), node);
}
}
/**
* 取得真实机器节点
*
* @param key
* @return
*/
public Node get(String key) {
if (circle.isEmpty()) {
return null;
}
Integer hash = hashFunction.hash(key);
if (!circle.containsKey(hash)) {
//沿环的顺时针找到一个虚拟节点
SortedMap<Integer, Node> tailMap = circle.tailMap(hash);
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
// 返回该虚拟节点对应的真实机器节点的信息
return circle.get(hash);
}
}



5、测试用例

package com.li.hash;
import com.li.hash.entity.Node;
import com.li.hash.sevice.ConsistentHash;
import com.li.hash.sevice.HashFunction;
import com.li.hash.sevice.impl.HashFunctionImpl;
import org.junit.Test;
import java.util.*;
public class test {
@Test
public void hashTest() {
// 机器节点IP前缀
final String IP_PREFIX = "192.168.1.";
// 每台真实机器节点上保存的记录条数
Map<String, Integer> map = new HashMap<String, Integer>();
HashFunction hashFunction = new HashFunctionImpl();
//真实物理节点
List<Node> realNodes = new ArrayList<>();
// 每台真实机器节点上保存的记录条数初始为0
for (int i = 1; i <= 10; i++) {
map.put(IP_PREFIX + i, 0);
Node node = new Node(IP_PREFIX + i, "node" + i);
realNodes.add(node);
}
ConsistentHash consistentHash = new ConsistentHash(hashFunction, 100, realNodes);
// 将10000条记录尽可能均匀的存储到10台机器节点
for (int i = 0; i < 1000000; i++) {
// 产生随机一个字符串当做一条记录,可以是其它更复杂的业务对象,比如随机字符串相当于对象的业务唯一标识
String data = UUID.randomUUID().toString() + i;
// 通过记录找到真实机器节点
Node node = consistentHash.get(data);
// 这里可以通过其它工具将记录存储真实机器节点上,比如MemoryCache等
// ...
// 每台真实机器节点上保存的记录条数加1
map.put(node.getIp(), map.get(node.getIp()) + 1);
}
// 打印每台真实机器节点保存的记录条数
for (int i = 1; i <= 10; i++) {
System.out.println(IP_PREFIX + i + "节点记录条数:" + map.get("192.168.1." + i));
}
}
}



最后,控制台打印的日志

192.168.1.1节点记录条数:86617
192.168.1.2节点记录条数:103035
192.168.1.3节点记录条数:118516
192.168.1.4节点记录条数:107492
192.168.1.5节点记录条数:99635
192.168.1.6节点记录条数:110879
192.168.1.7节点记录条数:99929
192.168.1.8节点记录条数:94695
192.168.1.9节点记录条数:85379
192.168.1.10节点记录条数:93823

经过多次打印和观察,10台服务器节点的数量,各不相同且与评价值10w有差异,这就说明了该算法的存储是负载不均衡的。



用户头像

李广富

关注

还未添加个人签名 2019.11.12 加入

还未添加个人简介

评论

发布
暂无评论
java实现一致性 hash 算法