一致性hash不做过多介绍了,其他地方寻址吧
为什么hash环长度范围是2"32 -1
一致性hash最初是用来解决ip映射问题;IP是32位,IP地址中间用 . 号隔开的每个数的取值范围是0—255,也就是256个数字,是2的8次方,用2进制表示即为8位,一共4个数字,即为32位
如何构建2的32次方hash环
首先我们得怎么构造这个2的32次方的hash环,方法有很多,我这里就直接推荐使用TreeMap这个数据结构,因为TreeMap底层是使用了红黑树结构来存储实体对象的,时间复杂度在O(logN),效率较高
hash算法的选择,保证散列后节点分布式均匀
我们在选择Hash算法上也需要选好,要尽可能的打散开,如果考虑简单的String.HashCode()方法,这个算法的缺点是,相似的字符串如N1(10.0.0.0:91001),N2(10.0.0.0:91002),N3(10.0.0.0:91003),哈希值也很相近,造成的结果是节点在Hash环上分布很紧密,导致大部分Key值落到了N0上,节点资源分布不均。一般我们采用FNV1_32_HASH、KETAMA_HASH等算法,KETAMA_HASH是MemCache集群默认的实现方法,这些算法效果要好得多,会使N0,N1,N2的Hash值更均匀的分布在环上
Ketama算法
Ketama 算法:将它称之为哈希算法其实不太准确,称之为一致性哈希算法可能更为合适,其他的哈希算法有通用的一致性哈希算法实现,只不过是替换了哈希方式而已,但 Ketama 是一整套的流程
算法代码
下面是简单写的一致性hash算法,md5散列代码和标准差测试代码,直接引用了几位能人之手,如下会标明出处,见谅。
- 一致性hash主算法
public class ConsistentHashingVirtualNode {
/**
* 虚拟节点树
*/
private SortedMap<Integer,String> vNodes = new TreeMap();
/**
* 真实节点
*/
private LinkedList<String> realNodes;
/**
* 一个真实节点对应的虚拟节点数量
*/
private int virtualNums;
/**
* 虚拟节点连接符
*/
private static final String VIRTUAL_NODE_SUFFIX = "-";
/**
* 构造器初始化hash环
* @param nodes
* @param vn
*/
public ConsistentHashingVirtualNode(LinkedList<String> nodes,int vn){
this.realNodes = nodes;
this.virtualNums = vn;
initP2Vmapping();
}
/**
* 构建hash环主方法,初始化虚拟节点与物理节点的映射关系;
* 映射的虚拟节点采用TreeMap排序,利用二叉树特性方便查找节点;
* map中key存储虚拟节点hash值,value存储对应的实体节点;虚拟节点的hash值来自于实体节点字符拼接;
*/
private void initP2Vmapping(){
for(String node:realNodes){
for( int i=0;i< virtualNums;i++ ){
String vName = getVirtualNodeNameByIndex(node,i);
vNodes.put(hash(vName),node);
}
}
}
/**
* 拼接虚机节点
* @param nodeName
* @param index
* @return
*/
private String getVirtualNodeNameByIndex(String nodeName, int index){
return new StringBuffer(nodeName)
.append(VIRTUAL_NODE_SUFFIX)
.append(index)
.toString();
}
/**
* hash算法,使用MD5散列。
* 这里直接拿了 https://github.com/itisaid/Doris/blob/master/common/doris.common/doris.algorithm/src/main/java/com/alibaba/doris/algorithm/util/MD5Util.java 的代码;
* @param k
* @return
*/
private int hash(String k) {
// String s = MD5Util.md5( k );
// return s.hashCode();
return MD5Util.md5HashCode(k);
}
/**
* 定位被路由到的实际节点
* @param p
* @return
*/
public String locate(String p){
SortedMap<Integer,String> toTailed = vNodes.tailMap(hash(p));
Integer key = toTailed.isEmpty()?vNodes.firstKey():toTailed.firstKey();
// System.out.println("virtual-node hash("+hash(p)+"): "+p+" -> realNode hash("+hash(vNodes.get(key))+"):"+vNodes.get(key));
return vNodes.get(key);
}
public static void main(String[] args) {
LinkedList<String> realNodes = new LinkedList<>();
realNodes.add("192.168.1.1");
realNodes.add("192.168.1.3");
realNodes.add("192.168.1.5");
realNodes.add("192.168.1.7");
realNodes.add("192.168.1.9");
ConsistentHashingVirtualNode consis
= new ConsistentHashingVirtualNode(realNodes,12);
System.out.println(consis.locate("1900000000000"));
}
}
-
MD5散列算法
public class MD5Util { private static MessageDigest md5Digist; private static final char[] hex = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; static { try { md5Digist = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } } /* * 构造md5 String * @param key * @return */ public static String md5(String key) { if (key == null) { key = ""; } try { MessageDigest messageDigest = (MessageDigest) md5Digist.clone(); messageDigest.update(key.getBytes()); byte[] b = messageDigest.digest(); char[] charArr = new char[32]; for (int i = 0; i < b.length; i++) { charArr[i * 2] = hex[b[i] >>> 4 & 0xF]; charArr[i * 2 + 1] = hex[b[i] & 0xF]; } return new String(charArr); } catch (CloneNotSupportedException e) { throw new RuntimeException("md5:" + e,e); } } /** * 使用MD5散列的hash code * * @param key * @return */ public static int md5HashCode(String key) { md5Digist.reset(); md5Digist.update(key.getBytes()); byte[] b = md5Digist.digest(); int rv = ((int) (b[3] & 0xFF) << 24) | ((int) (b[2] & 0xFF) << 16) | ((int) (b[1] & 0xFF) << 8) | (b[0] & 0xFF); return rv > 0 ? rv : -rv; } }
-
标准差算法工具
public class StatisticsUtil { // 方差 s^2=[(x1-x)^2 +...(xn-x)^2]/n public static double variance(Long[] x) { int m = x.length; double sum = 0; for (int i = 0; i < m; i++) { // 求和 sum += x[i]; } double dAve = sum / m;// 求平均值 double dVar = 0; for (int i = 0; i < m; i++) { // 求方差 dVar += (x[i] - dAve)* (x[i] - dAve); } return dVar / m; } // 标准差σ=sqrt(s^2) public static double standardDeviation(Long[] x) { int m = x.length; double sum = 0; for (int i = 0; i < m; i++) { // 求和 sum += x[i]; } double dAve = sum / m;// 求平均值 double dVar = 0; for (int i = 0; i < m; i++) { // 求方差 dVar += (x[i] - dAve)* (x[i] - dAve); } return Math.sqrt(dVar / m); } }
-
单元测试
public class ConsistentHashTest { /** * 构建实体ips */ static String[] ips = { "192.111.1.0","192.111.1.2","192.111.1.3", "192.172.2.2","192.172.1.2", "192.172.3.2", "192.168.3.2","192.168.1.2","192.168.1.2", "192.117.4.211"}; // 10 台随机 ip private void testDistribution(int num) { /** * 实体节点加个端口|无相关 */ LinkedList<String> realNodes = new LinkedList<>(); for (String ip : ips) { realNodes.add(ip+":8080"); } ConsistentHashingVirtualNode chloadBalance = new ConsistentHashingVirtualNode(realNodes,100000); // 构造 10000 随机请求 List<String> invocations = new ArrayList<>(); for (int i = 0; i < 10000; i++) { invocations.add(UUID.randomUUID().toString()); } // 统计分布 AtomicLongMap<String> atomicLongMap = AtomicLongMap.create(); for (String server : realNodes) { atomicLongMap.put(server, 0); } for (String invocation : invocations) { String selectedServer = chloadBalance.locate(invocation); atomicLongMap.getAndIncrement(selectedServer); } // System.out.println("10个物理节点,100w个虚拟节点,10000次随机请求,第"+num+"次测试方差:"+StatisticsUtil.variance(atomicLongMap.asMap().values().toArray(new Long[]{}))); System.out.println("10个物理节点,100w个虚拟节点,10000次随机请求,第"+num+"次测试标准差:"+StatisticsUtil.standardDeviation(atomicLongMap.asMap().values().toArray(new Long[]{ }))); //https://www.cnkirito.moe/spring-security-6/ } @Test public void testRange(){ for(int i=1;i<=20;i++){ testDistribution(i); } } }