分布式系统中负载均衡的问题时候可以使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载均衡的作用。比如说分布式缓存,既然是缓存,就没有必要去做一个所有机器上的数据都完全一样的缓存集群,而是应该设计一套好的缓存路由工具类,所以一致性Hash算法就因此而诞生了。
衡量一个一致性Hash算法最重要的两个特征:
①平衡性:平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。
②单调性:单调性是指如果已经有一些数据通过哈希分配到了相应的机器上,又有新的机器加入到系统中。哈希的结果应能够保证原有的数据要么还是呆在它所在的机器上不动,要么被迁移到新的机器上,而不会迁移到旧的其他机器上。
业界常用的两种一致性Hash算法,一种是不带虚拟节点的Hash算法,另外一种是带虚拟节点的Hash算法。
下面的代码就是不带虚拟节点的一致性Hash算法,原理稍后分析:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import org.apache.commons.lang.StringUtils;
/**
* 一致性Hash算法
* */
public class ConsistentHashWithoutVirtualNode {
/**
* 服务器节点信息
* */
private static SortedMap nodeMap = new TreeMap<>();
//服务器配置信息(可配置)
private static String[] servers = {"192.168.56.120:6379",
"192.168.56.121:6379",
"192.168.56.122:6379",
"192.168.56.123:6379",
"192.168.56.124:6379"};
/**
* 初始化
* */
static{
for(int i=0; i< servers.length; i++){
nodeMap.put(getHash(servers[i]), servers[i]);
}
System.out.println("Hash环初始化完成!");
}
/**
* 经典的Time33 hash算法
* */
public static int getHash(String key) {
if(StringUtils.isEmpty(key))
return 0;
try{
MessageDigest digest = MessageDigest.getInstance("MD5");
key = new String(digest.digest(key.getBytes()));
}catch(NoSuchAlgorithmException e){
e.printStackTrace();
}
int hash = 5381;
for (int i = 0; i < key.length(); i++) {
int cc = key.charAt(i);
hash += (hash << 5) + cc;
}
return hash<0 ? -hash : hash;
}
/**
* 缓存路由算法
* */
public static String getServer(String key){
int hash = getHash(key);
//得到大于该Hash值的所有Map
SortedMap subMap = nodeMap.tailMap(hash);
if(subMap.isEmpty()){
int index = nodeMap.firstKey();
System.out.printf("%s被路由到节点[%s]\n", key, nodeMap.get(index));
return nodeMap.get(index);
}else{
int index = subMap.firstKey();
System.out.printf("%s被路由到节点[%s]\n", key, nodeMap.get(index));
return nodeMap.get(index);
}
}
/**
* 使用UUID模拟随机key
* */
public static void main(String[] args) {
for(int i=0; i<20; i++){
String str = UUID.randomUUID().toString();
getServer(str);
}
}
}
首先,针对平衡性,我们需要选择一个好的Hash函数,我们选择的Hash算法是业界内比较出名的Time33 Hash算法,这个你们可以百度一下。但是Time33 Hash算法有一个弊端,那就是对于两个key差不多的字符串来说,他们生成的Hash值很接近,所以我们的解决办法就是在生成Hash值之前先用MD5算法取一次信息指纹。
nodeMap是用来保存服务器节点信息的SortedMap(key是hash值,value是服务器节点信息); servers是服务器的配置信息;使用static静态代码块初始化nodeMap保存节点信息。
缓存路由算法是核心代码,大概思想是先计算key的hash值,然后用hash值找到nodeMap中的所有键值大于该hash值的键值对,如果找到,取键值对最小的那个键值对的值作为路由结果;如果没有找到键值对的键大于该hash值的键值对,那么就取nodeMap里键值对的键最小的那个值作为路由结果。
接下来,我们使用UUID生成随机字符串测试一下吧,测试结果如下:
但是这种不带虚拟节点的路由算法有个问题,在增减机器时会使旧的数据大量“失效”,也称为命中率下降。
于是我们选择把一个机器分为很多个虚拟节点,并且使这些虚拟节点交叉的分散在一个hash环上,经过改良后的代码如下:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import org.apache.commons.lang.StringUtils;
public class ConsistentHashWithVirtualNode {
/**
* 虚拟节点信息
* key:hash值
* value:真实节点+"&"+序号
* */
private static SortedMap virtualNodeMap = new TreeMap<>();
//单机虚拟节点
private static final int VIRTUAL_NODE_NUM = 5;
//服务器配置信息(可配置)
private static String[] servers = {"192.168.56.120:6379",
"192.168.56.121:6379",
"192.168.56.122:6379",
"192.168.56.123:6379",
"192.168.56.124:6379"};
/**
* 初始化
* */
static{
for(int i=0; i< servers.length; i++){
for(int j=0; j subMap = virtualNodeMap.tailMap(hash);
if(subMap.isEmpty()){
int index = virtualNodeMap.firstKey();
System.out.printf("%s被路由到虚拟节点[%s]真实节点[%s]\n", key, virtualNodeMap.get(index),
virtualNodeMap.get(index).substring(0, virtualNodeMap.get(index).indexOf("&")));
return virtualNodeMap.get(index).substring(0, virtualNodeMap.get(index).indexOf("&"));
}else{
int index = subMap.firstKey();
System.out.printf("%s被路由到虚拟节点[%s]真实节点[%s]\n", key, virtualNodeMap.get(index),
virtualNodeMap.get(index).substring(0, virtualNodeMap.get(index).indexOf("&")));
return virtualNodeMap.get(index).substring(0, virtualNodeMap.get(index).indexOf("&"));
}
}
/**
* 使用UUID模拟随机key
* */
public static void main(String[] args) {
for(int i=0; i<20; i++){
String str = UUID.randomUUID().toString();
getServer(str);
}
}
}
虚拟节点的大致思想之这样的,使用真实节点+"&"+序号(序号的范围是0到单台服务器所需的虚拟节点个数VIRTUAL_NODE_NUM)作为虚拟节点的值,核心代码如上面截图所示。
接下来,我们还是使用UUID生成随机字符串测试一下吧,测试结果如下:
好的,大功告成,以上就是关于一致性Hash路由算法的全部内容。PS:以上代码有删减,仅提取了其中的核心代码。如果喜欢我的内容的话,欢迎转发,谢谢。
欢迎大家关注我的微信公众号"Java架构师养成记",不定期分享各类面试题、采坑经历。