一致性Hash算法

应用场景

在节点集群部署环境,需要对大量的请求进行均匀分发以达到负载均衡的请求。

如果是简单的对节点数量进行取模映射,当增删节点的时候,原有的映射结果,绝大部分会失效,拓展性太差。

此时就需要使用一致性 Hash 算法,减少在服务器数量变化的时候造成的数据迁移影响。

一致哈希 是一种特殊的哈希算法。在使用一致哈希算法后,哈希表槽位数(大小)的改变平均只需要 K/N 个关键字重新映射,其中 K 是关键字的数量,N 是槽位数量。然而在传统的哈希表中,添加或删除一个槽位的几乎需要对所有关键字进行重新映射。

原理

upload successful

  1. 首先求出服务器(节点)的哈希值,并将其配置到 [0, 2^32-1] 的圆上。

  2. 然后采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上。

  3. 然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。

  4. 如果超过 2^32 仍然找不到服务器,就会保存到第一台服务器上。

如图所示,节点 Node0,Node1,Node2 ,如果数据被映射到 Node0 至 Node2 之间,那么数据将会最终对应到 Node2 节点上。

可能存在问题

upload successful

数据倾斜:服务节点太少,Hash 规则将大部分的请求映射到同一台服务器上,使得某一个节点压力大。如图所示,只有两个节点,绝大部分请求将会映射到 N1

upload successful

解决方案:将一个物理节点拆分为多个虚拟节点,并且同一个物理节点的虚拟节点尽量均匀分布在 Hash 环上。A 添加虚拟节点 A’,如果映射到 A’,那么最终会请求节点 A。

Java 实现(简单版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class DemoA {

// 真实节点列表,假设有 5 个节点
   public static String[] hosts = {"192.168.0.0",
"192.168.0.1",
"192.168.0.2",
"192.168.0.3",
"192.168.0.4",
"192.168.0.5"};

   // 存储节点(真实+虚拟),并且能根据 key 排序
   private static SortedMap<Integer,String> sortedMap = new TreeMap<>();

// 初始化加入节点,虚拟节点根据真实节点构建
   static {
for (String ip:hosts) {
sortedMap.put(getHash(ip),ip);
System.out.println(ip + ":" + getHash(ip));
for (int i = 0; i < 1000; i++) {
          // 虚拟节点只是后面加上字符串,取的时候方便根据虚拟节点找到真实节点
               sortedMap.put(getHash(ip + "&" + i),ip + "&" + i);
}
}
}

// 将字符串转为 int 类型,网上抄的方法
   public 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 String get(String key){
  // 数据映射到环的值 A
       int hash = getHash(key);
String finalIP = "";
Integer i;
       // tailMap 返回比 hash 值大的子 Map
       SortedMap<Integer,String> map = sortedMap.tailMap(hash);
       // 如果环内没有比 hash 值大的,那么返回环的第一个节点
       if (subMap.isEmpty()){
i = sortedMap.firstKey();
finalIP = sortedMap.get(i);
}else {
      // 返回子 Map 的第一个节点
           i = subMap.firstKey();
finalIP = subMap.get(i);
}
       // 如果是虚拟节点,那么找到其对应真实节点
       if (finalIP.indexOf("&") > 0){
finalIP = finalIP.substring(0,finalIP.indexOf("&"));
}
return finalIP;
}

public static void main(String[] args) {
String[] key = {"tes1","测试","HASH"};
for (String s:key) {
System.out.println(s + " hash is:" + getHash(s) + " to:" + get(s));
}
}
}

博客参考

对一致性Hash算法,Java代码实现的深入研究

面试必备:什么是一致性Hash算法?