分布式系统中负载均衡的问题时候可使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载均衡的做用。好比说分布式缓存,既然是缓存,就没有必要去作一个全部机器上的数据都彻底同样的缓存集群,而是应该设计一套好的缓存路由工具类,因此一致性Hash算法就所以而诞生了。java
衡量一个一致性Hash算法最重要的两个特征:node
①平衡性:平衡性是指哈希的结果可以尽量分布到全部的缓冲中去,这样可使得全部的缓冲空间都获得利用。面试
②单调性:单调性是指若是已经有一些数据经过哈希分配到了相应的机器上,又有新的机器加入到系统中。哈希的结果应可以保证原有的数据要么仍是呆在它所在的机器上不动,要么被迁移到新的机器上,而不会迁移到旧的其余机器上。算法
业界经常使用的两种一致性Hash算法,一种是不带虚拟节点的Hash算法,另一种是带虚拟节点的Hash算法。apache
下面的代码就是不带虚拟节点的一致性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<Integer, String> 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<Integer, String> 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算法取一次信息指纹。bash
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<Integer, String> 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<VIRTUAL_NODE_NUM; j++){
String virtualNodeName = servers[i] + "&" + j;
virtualNodeMap.put(getHash(virtualNodeName), virtualNodeName);
}
}
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<Integer, String> 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架构师养成记",不按期分享各种面试题、采坑经历。