一致性哈希算法在1997年被提出,英文名叫constant hahsing,目前这种算法被广泛的应用到了cache系统中。
在cache系统中如何保证在添加或者较少节点服务器时尽可能减少数据的移动是一大挑战,这要求负责路由的hash算法具有很高的单调性。Constant hashing就是满足需求的算法之一。
一般情况下,hash算法会将值映射到32位的数值空间,我们可以将这个空间想象成一个首尾相接的圆环,如下面那样。
假设我们有4个对象object1~object4,且他们的hash值在圆环上的分布情况如下图
Consistent hashing 的基本思想就是将cache服务器和存储对象使用相同的hash函数映射到同一个 圆环空间上。 假设有A、B、C三台cache服务器,将他们的机器名或者IP地址作为输入值计算hash值 ,将其也映射到圆环空间上,结果如下:
在这个圆环空间中,按照顺时针的方向,对象存储在离它最近的cache服务器上。比如object1存在cacheA中,object4存在cacheB中,object2和object3存在cacheC中。
考虑假设cache B挂掉了,根据上面映射方法,只有cacheB上的数据需要移动,即object4移动到cacheC,其他的对象保持不变。
再考虑添加一台cache D的情况,假设cache D被映射在对象object2和object3之间。这时受影响的将仅是那些沿 cache D 逆时针遍历直到下一个 cache之间的对象 。即将object2映射到cache D上。
单调性解决了,但是可能会出现热点现象,即存储在各个cache服务器上的数据存在不平衡。为了解决这种情况,一致性哈希算法引入了“虚拟节点”的概念。虚拟节点( virtual node )是实际节点在圆环空间的复制品,一个节点对应了若干个虚拟节点。如图,圆环上部署了cacheA和cacheC两台服务器,出现了数据不平衡,为了保持平衡,我们引入两个虚拟节点,
此时,对象到“虚拟节点”的映射关系为: objec1->cache A2;objec2->cache A1; objec3->cache C1 ;objec4->cache C2 。 因此对象object1和object2 都被映射到了cache A上,而object3和object4 映射到了 cache C 上,平衡性有了很大提高。
引入虚拟节点后,映射关系就从{ 对象 ->节点 } 转换到了{ 对象-> 虚拟节点 } 。
下面是constant hashing的一个完整实现:
package algorithms;
import java.util.Collection;
import java.util.SortedMap;
import java.util.TreeMap;
public class ConsistentHash<T> {
/**
* 计算使用的hash函数,推荐使用MD5
*/
private final HashFunction hashFunction;
/**
* 虚拟节点的倍数
*/
private final int numberOfReplicas;
/**
* cache节点环
*/
private final SortedMap<Integer, T> circle = new TreeMap<Integer, T>();
public ConsistentHash(HashFunction hashFunction, int numberOfReplicas,
Collection<T> nodes) {
this.hashFunction = hashFunction;
this.numberOfReplicas = numberOfReplicas;
for (T node : nodes) {
add(node);
}
}
/**
* @description 添加ceche服务器节点
* @param node 服务器节点,可以用服务器的IP表示
* @add date 2011-10-30
*/
public void add(T node) {
for (int i = 0; i < numberOfReplicas; i++) {
circle.put(hashFunction.hash(node.toString() + i), node);
}
}
/**
* @description 删除服务器节点
* @param node 服务器节点,可以用服务器的IP表示
* @add date 2011-10-30
*/
public void remove(T node) {
for (int i = 0; i < numberOfReplicas; i++) {
circle.remove(hashFunction.hash(node.toString() + i));
}
}
/**
* @description 获取对象对应的cache服务器
* @param key 需要存储的对象
* @return 目标cache服务器
* @add date 2011-10-30
*/
public T get(Object key) {
if (circle.isEmpty()) {
return null;
}
int hash = hashFunction.hash(key);
if (!circle.containsKey(hash)) {
//顺时针查找最近的节点
SortedMap<Integer, T> tailMap = circle.tailMap(hash);
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
return circle.get(hash);
}
}