一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题。一致性哈希修正了CARP使用的简单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。
在分布式集群环境当中,机器的添加、删除以及产生故障自动脱离集群这是最基本的功能,如果采用hash(o)%n的算法,在机器数量有变动的时候,以前的数据基本是找不到的。比如最开始3台 hash(o)%3=2 如果增加了一台hash(o)%4=? 结果肯定不会为2,如果使用hash取模,在机器数量增减的时候该问题是无法避免的。为了解决这个问题,就产生了一致性hash算法
构造一个2^32的整数环,即0~(2^32-1)的数字空间,形成一个环,起点为0,终点为2^32-1,如下图。按逆时针分布
假如现在有3个对象o1 o2 o3,使用hash函数计算对应的hash值(0~2^32-1范围内) 我们把它们放在环上。同样计算出3台的机器s1 s2 s3的hash值放入到环上
对象为o1 o2 o3 机器s1 s2 s3
现在为对象选择机器:
这里为顺时针,对象o3顺时针查找到s2,即对象o3存储在s2这台机器上的,最后对象o1则存储在s3,如下图
当其中一个服务器挂掉了,比如s3挂了。o1被重新分配到了s2
如果增加了机器,同理也会进行相应的负载
java代码的实现:
//待添加入Hash环的服务器列表
private static String[] servers = {"192.168.0.0:111", "192.168.0.1:111",
"192.168.0.2:111", "192.168.0.3:111", "192.168.0.4:111"};
//key表示服务器的hash值,value表示服务器
private static SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>();
//程序初始化,将所有的服务器放入sortedMap中
static {
for (int i = 0; i < servers.length; i++) {
int hash = getHash(servers[i]);
System.out.println("[" + servers[i] + "]加入集合中, 其Hash值为" + hash);
sortedMap.put(hash, servers[i]);
}
System.out.println();
}
//得到应当路由到的结点
private static String getServer(String key) {
//得到该key的hash值
int hash = getHash(key);
//得到大于该Hash值的所有Map
SortedMap<Integer, String> subMap = sortedMap.tailMap(hash);
if (subMap.isEmpty()) {
//如果没有比该key的hash值大的,则从第一个node开始
Integer i = sortedMap.firstKey();
//返回对应的服务器
return sortedMap.get(i);
} else {
//第一个Key就是顺时针过去离node最近的那个结点
Integer i = subMap.firstKey();
//返回对应的服务器
return subMap.get(i);
}
}
//使用FNV1_32_HASH算法计算服务器的Hash值,这里不使用重写hashCode的方法,最终效果没区别
private static int getHash(String str) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < str.length(); i++)
hash = (hash ^ str.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;
}
public static void main(String[] args) {
String[] keys = {"太阳", "月亮", "星星"};
for (int i = 0; i < keys.length; i++) {
System.out.println("[" + keys[i] + "]的hash值为" + getHash(keys[i])
+ ", 被路由到结点[" + getServer(keys[i]) + "]");
}
}
[192.168.0.0:111]加入集合中, 其Hash值为575774686
[192.168.0.1:111]加入集合中, 其Hash值为8518713
[192.168.0.2:111]加入集合中, 其Hash值为1361847097
[192.168.0.3:111]加入集合中, 其Hash值为1171828661
[192.168.0.4:111]加入集合中, 其Hash值为1764547046
[太阳]的hash值为1977106057, 被路由到结点[192.168.0.1:111]
[月亮]的hash值为1132637661, 被路由到结点[192.168.0.3:111]
[星星]的hash值为880019273, 被路由到结点[192.168.0.3:111]
有重复服务器使用
虚拟节点:
虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一实际个节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列
个人理解虚拟节点就是为了提高服务器命中率,分布得更均匀
如下图:
上图的s11 s22 s33分别是不同机器的虚拟节点,可以很多个,分布在hash环上,还是以hash函数的hash值来确定数字空间
带虚拟节点的:
//待添加入Hash环的服务器列表
private static String[] servers = {"192.168.0.0:111", "192.168.0.1:111",
"192.168.0.2:111", "192.168.0.3:111", "192.168.0.4:111"};
//真实节点列表
private static List<String> realNodes = new LinkedList<>();
//虚拟节点列表
private static SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>();
private static final int NUM_HOST = 5;
//程序初始化,将所有的服务器放入sortedMap中
static {
//添加真实节点
for (int i = 0; i < servers.length; i++) {
realNodes.add(servers[i]);
}
//添加虚拟节点
for (String str : realNodes) {
for (int i = 1; i <= NUM_HOST; i++) {
String nodeName = str + "VM" + String.valueOf(i);
int hash = getHash(nodeName);
sortedMap.put(hash, nodeName);
System.out.println("虚拟节点hash:" + hash + "【" + nodeName + "】放入");
}
}
}
//得到应当路由到的结点
private static String getServer(String key) {
//得到该key的hash值
int hash = getHash(key);
//得到大于该Hash值的所有Map
String host;
SortedMap<Integer, String> subMap = sortedMap.tailMap(hash);
if (subMap.isEmpty()) {
//如果没有比该key的hash值大的,则从第一个node开始
Integer i = sortedMap.firstKey();
//返回对应的服务器
host = sortedMap.get(i);
} else {
//第一个Key就是顺时针过去离node最近的那个结点
Integer i = subMap.firstKey();
//返回对应的服务器
host= subMap.get(i);
}
if (StringUtils.isNotBlank(host)) {
String realHost = host.substring(0,host.indexOf("VM"));
System.out.println(realHost);
return realHost;
}
return null;
}
//使用FNV1_32_HASH算法计算服务器的Hash值
private static int getHash(String str) {
// int hash = str.hashCode();
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < str.length(); i++) {
hash = (hash ^ str.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;
}
public static void main(String[] args) {
String[] keys = {"天下", "无敌", "的我"};
for (int i = 0; i < keys.length; i++) {
System.out.println("[" + keys[i] + "]的hash值为" + getHash(keys[i]) + ", 被路由到结点[" + getServer(keys[i]) + "]");
}
}
虚拟节点hash:203600595【192.168.0.0:111VM1】放入
虚拟节点hash:533864743【192.168.0.0:111VM2】放入
虚拟节点hash:283014282【192.168.0.0:111VM3】放入
虚拟节点hash:112805468【192.168.0.0:111VM4】放入
虚拟节点hash:2047670539【192.168.0.0:111VM5】放入
虚拟节点hash:519865065【192.168.0.1:111VM1】放入
虚拟节点hash:1154856732【192.168.0.1:111VM2】放入
虚拟节点hash:1253620420【192.168.0.1:111VM3】放入
虚拟节点hash:271923136【192.168.0.1:111VM4】放入
虚拟节点hash:954403340【192.168.0.1:111VM5】放入
虚拟节点hash:1573046012【192.168.0.2:111VM1】放入
虚拟节点hash:1216691265【192.168.0.2:111VM2】放入
虚拟节点hash:1427550122【192.168.0.2:111VM3】放入
虚拟节点hash:563194059【192.168.0.2:111VM4】放入
虚拟节点hash:1191243314【192.168.0.2:111VM5】放入
虚拟节点hash:449809950【192.168.0.3:111VM1】放入
虚拟节点hash:1517398358【192.168.0.3:111VM2】放入
虚拟节点hash:1061446677【192.168.0.3:111VM3】放入
虚拟节点hash:1711703068【192.168.0.3:111VM4】放入
虚拟节点hash:888576886【192.168.0.3:111VM5】放入
虚拟节点hash:300861392【192.168.0.4:111VM1】放入
虚拟节点hash:185778140【192.168.0.4:111VM2】放入
虚拟节点hash:70846991【192.168.0.4:111VM3】放入
虚拟节点hash:20072546【192.168.0.4:111VM4】放入
虚拟节点hash:294895133【192.168.0.4:111VM5】放入
192.168.0.0:111
[天下]的hash值为1815790460, 被路由到结点[192.168.0.0:111]
192.168.0.3:111
[无敌]的hash值为705568906, 被路由到结点[192.168.0.3:111]
192.168.0.4:111
[的我]的hash值为2055637786, 被路由到结点[192.168.0.4:111]
这样分布得更均匀,每个负载比较平均