一致性哈希算法
在分布式系统中,不同对象的数据需要存储到不同的服务器中,我们一般使用某种哈希算法确定数据存储到哪台服务器上。
简单哈希算法
最简单的,我们通过以下公司确定对象存储到哪台服务器上:
其中o表示待存储的对象,m表示服务器的数量,n表示对象o存储的服务器编号。
乍看起来,实现简单好像也很合理,但是考虑如下场景:
3台服务器编号分表为0,1,2,10个对象的哈希值分别为0~9,所以存储关系为:
服务器0:0,3,6,9
服务器1:1,4,7
服务器2:2,5,8
假如此时添加一台编号为3的服务器,存储关系需要更变为:
服务器0:0,4,8
服务器1:1,5,9
服务器2:2,6
服务器3:3,7
其中加粗的对象表示需要迁移的对象。可以发现新增的服务器3上仅新增了2个对象,但是整体却有7个对象发生了迁移。
这种简单的哈希算法,实现上很简单,但是当发生服务器节点的新增或移除时,却会发生很多没有必要的数据迁移,占用大量的机器和网络资源,轻则影响迁移时的整体性能,重则直接导致服务不可用。
一致性哈希算法
一致性哈希算法就是为了解决上面的问题而出现了,它可以保证在服务器节点添加或者移除时,不会发生全局的数据迁移,而是将迁移局限在一个节点上。
一致性哈希算法将0~2^32-1的整数型哈希值映射到一个环形结构上,环形结构的开头为0,然后沿着顺时针方式依次映射逐渐增大的整数型哈希值,直到环形结构的末尾映射2^32-1,然后首尾是连接到一起的。
每一个待储存对象都可以根据其哈希值映射到上边的环上,每一台服务器也可以通过其哈希值映射到上边的环上,如下图:
对于每个待储存的对象,从其映射到环上的节点逆时针方向寻找,找到的第一个服务器节点就是该对象应该存储的服务器。如上图中,数据1和数据2存储在服务器1上,数据3和数据4存储在服务器2上,数据5和数据6存储在服务器3上。这种算法的好处是,当服务器节点发生增删,不会发生数据的大规模迁移。
比如我们把服务器2移除,只需要把服务器2上的数据3和数据4迁移到服务器1,不需要其他不必要的迁移,如下图:
又比如我们在增加一个服务器节点服务器1-1,映射到哈希上的位置位于数据1和数据2之间的位置,那么只需要将原本在服务器1上的数据2迁移到新的服务器,也不需要做其他不必要的迁移,如下图:
虚拟节点
在实际生产环境 ,服务器的数量总不会是特别大的数值,这时候有很大的概率发生倾斜。
如上图,当服务器1和服务器2映射到哈希环的位置离得太近时,服务器2负责了数据1~4的数据,而服务器1上没有数据,真的达到了闲的闲死,忙的忙死的境界。
为了解决这种问题,虚拟节点的概念被引入。如果每个服务器映射到哈希环的多个位置,那么出现上述闲的闲死,忙的忙死的概率会小的多。当服务器1只映射到哈希环的一个节点上时,发生服务服务器1“躲在”别的服务器后边的概率会较大,但当我把服务器1映射到哈希环的多个节点上,多个节点都“躲在”别的服务器上的概率会小的多,并且虚拟节点越多,数据的分配应该越趋于均衡。
引入虚拟节点还有一个好处是,当发生服务器节点的增删时,数据的迁移也会趋向于更均衡。比如服务器只映射到一个节点上时,当发生服务器的移除,那么被移除服务器上的数据需要全部迁移到逆时针方向离这个服务器最近的服务器节点。而当每个服务器有多个虚拟节点时,发生服务器节点移除时,需要移除多个服务器虚拟节点,每个服务器虚拟节点逆时针方向都有一个其他服务器的虚拟节点,他们大概率不会是同一个服务器,从而实现均衡地迁移到多个服务器。
Demo实现
/**
* 用于求哈希值
*/
package hashtest;
public class HashCodeGetter {
public static final HashCodeGetter SINGLE_HASH_CODE_GETTER = new HashCodeGetter();
private static final long FNV_32_INIT = 2166136261L;
private static final int FNV_32_PRIME = 16777619;
public int getHashCode(String origin) {
final int p = FNV_32_PRIME;
int hash = (int)FNV_32_INIT;
for (int i = 0; i < origin.length(); i++) {
hash = (hash ^ origin.charAt(i)) * p;
}
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
hash = Math.abs(hash);
return hash;
}
}
/**
* hash算法容器接口
*/
package hashtest;
public interface HashContainer {
//向容器中添加服务器
boolean addServer(String serverName);
//移容器中指定服务器
boolean delServer(String serverName);
//根据数据返回数据应该在的服务器的名称
String getServerName(String data);
}
/**
* 没使用虚拟节点的实现
*/
package hashtest;
import java.util.Map;
import java.util.TreeMap;
public class BasicHashContainer implements HashContainer {
//使用TreeMap保存哈希值与服务器的映射,
//TreeMap提供了floorEntry()方法,
//可以在log(n)的复杂度内返回数据节点逆时针最近的服务器节点
private TreeMap<Integer, String> treeMap = new TreeMap<>();
@Override
public boolean addServer(String serverName) {
int hash = HashCodeGetter.SINGLE_HASH_CODE_GETTER.getHashCode(serverName);
treeMap.put(hash, serverName);
return true;
}
@Override
public boolean delServer(String serverName) {
int hash = HashCodeGetter.SINGLE_HASH_CODE_GETTER.getHashCode(serverName);
treeMap.remove(hash);
return true;
}
@Override
public String getServerName(String data) {
int hash = HashCodeGetter.SINGLE_HASH_CODE_GETTER.getHashCode(data);
Map.Entry<Integer, String> resultEntry = treeMap.floorEntry(hash);
if (resultEntry == null) {
resultEntry = treeMap.lastEntry();
}
return resultEntry.getValue();
}
}
/**
* 使用虚拟节点的实现.
*/
package hashtest;
import java.util.Map;
import java.util.TreeMap;
public class BetterHashContainer implements HashContainer {
private final int VIRTUAL_NODE_NUM = 50;
private TreeMap<Integer, String> treeMap = new TreeMap<>();
@Override
public boolean addServer(String serverName) {
for (int i = 0; i < VIRTUAL_NODE_NUM; ++i) {
String virtualNodeName = serverName + "VN" + i;
treeMap.put(HashCodeGetter.SINGLE_HASH_CODE_GETTER.getHashCode(virtualNodeName), serverName);
}
return true;
}
@Override
public boolean delServer(String serverName) {
for (int i = 0; i < VIRTUAL_NODE_NUM; ++i) {
String virtualNodeName = serverName + "VN" + i;
treeMap.remove(HashCodeGetter.SINGLE_HASH_CODE_GETTER.getHashCode(virtualNodeName));
}
return true;
}
@Override
public String getServerName(String data) {
int hash = HashCodeGetter.SINGLE_HASH_CODE_GETTER.getHashCode(data);
Map.Entry<Integer, String> resultEntry = treeMap.floorEntry(hash);
if (resultEntry == null) {
resultEntry = treeMap.lastEntry();
}
return resultEntry.getValue();
}
}