一致性哈希算法
一致性哈希算法(Consistent Hasing,以下简称CH)是一种特殊的哈希算法,使用CH的哈希表扩容的时候,平均只有个关键字需要被重新映射(或者移动),这里的是关键字的数量,指的是哈希表的槽位(slot)。
而传统的哈希表在扩容的时候,几乎所有的关键字需要被重新映射(或移动)。
传统的哈希表的工作流程如下:
设一个哈希表有个slot
,每个关键字key通过哈希函数得到一个哈希值h
,然后通过取模h%n
得到所在的slot
位置。
当哈希表扩容的时候,上述的n
发生了变化,因此原来的关键字和slot
之间的映射关系发生了变化,现在需要重新建立这种关系。当这个哈希表特别大的时候,涉及到的关键字特别多,这种扩容带来的代价十分巨大。
基于传统哈希表的这个局限,一致性哈希算法通过将映射后的空间组织成一个哈希环Circle
,每个环上有多个结点Node
,因此结点将映射后的空间划分成了多段,当新增或者删除结点的时候,只需要重新映射某一段的数据即可。
CH常常作为一种负载均衡算法,在很多地方都得到了应用。
关键字的定位
示意图如下:
上述示意图描述了一个具有8个节点的哈希环,当一个关键字需要定位到某个节点的时候,首先根据哈希函数计算关键字的哈希值h
,然后这个哈希值必定落在某个区间里面,顺时针找到第一个大于该哈希值的节点,将关键字存储到该结点。
节点删除
当由于主机宕机、负载过高等原因,需要暂时将某个节点从环中删除,此时CH不需要改变所有的数据映射的关系,而只是修改部分数据的映射关系即可。
示意图如下:
当节点sn1
被删除的时候,哈希值落在sn0到sn1之间的关键字在定位的时候会定位到sn2
,因此需要将sn1
的数据拷贝到sn2
节点上。
节点增加
同节点删除类似,示意图如下:
当在sn1和sn2之间增加了节点sn8
,需要将sn2的数据迁移到sn8节点上。
虚拟节点
当哈希环上的节点的数量比较少的时候,可能会出现节点在环上分布不均匀,例如出现以下这种情况:
这种情况必然会造成大量的请求转发给服务器1,造成服务器1不堪重负,而服务器2却无所事事,这就是所谓的负载不均衡。
因此在环上节点分布不均匀的时候,可以增加若干个虚拟节点来有效的解决这个负载不均衡的问题。
于是可以增加“Server 1#1”、“Server 1#2”、“Server 1#3”、“Server 2#1”、“Server 2#2”、“Server 2#3”,于是形成六个虚拟节点,这样在真实节点少的情况下,可以将空间划分得更加均匀一点。
代码实现
public class ConsistentHash<T> {
interface HashFunction {
int hash(String s);
}
private int numerOfVirtualNode;
private SortedMap<Integer, T> hashCircle = new TreeMap<>();
private HashFunction hashFunction;
public ConsistentHash (int numerOfVirtualNode, Collection<T> nodes) {
this(null, numerOfVirtualNode, nodes);
}
public ConsistentHash (HashFunction hf, int numerOfVirtualNode, Collection<T> nodes) {
this.numerOfVirtualNode = numerOfVirtualNode;
this.hashFunction = hf;
for (T node : nodes) {
add(node);
}
}
/**
* 增加一个节点
* @param node
*/
public void add(T node) {
// TODO Auto-generated method stub
for (int i = 0; i < numerOfVirtualNode; i++) {
hashCircle.put(hashFunction.hash(node.toString() + i), node);
}
//这里省略了增加节点以后,数据迁移的操作。
}
/**
* 删除一个节点
* @param node
*/
public void remove(T node) {
for (int i = 0; i < numerOfVirtualNode; i++) {
hashCircle.remove(hashFunction.hash(node.toString() + i));
}
//这里省略了删除节点以后,数据迁移的操作。
}
/**
* 顺时针寻找第一个大于key的hash值的节点
* @param key
* @return
*/
public T get(Object key) {
if (hashCircle.isEmpty()) {
return null;
}
int hash = hashFunction.hash(key.toString());
if (!hashCircle.containsKey(hash)) {
SortedMap<Integer, T> tailMap = hashCircle.tailMap(hash);
hash = tailMap.isEmpty() ? hashCircle.firstKey() : tailMap.firstKey();
}
return hashCircle.get(hash);
}
//default hash function
static class MD5HashFunction implements HashFunction {
MessageDigest md5 = null;
public MD5HashFunction() {
// TODO Auto-generated constructor stub
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public int hash(String s) {
// TODO Auto-generated method stub
md5.reset();
byte[] res = md5.digest(s.getBytes());
int hash = ((res[0] & 0xFF) << 24) | ((res[1] & 0xFF) << 16)| ((res[2] & 0xFF) << 8) | ((res[3] & 0xFF));
return hash;
}
}
}