举个例子,有5台服务器,编号分别是0(A),1(B),2(C),3(D),4(E) ,正常情况下,假设用户数据hash值为12,那么对应的数据应该缓存在12%5=2号服务器上,假设编号为3的服务器此时挂掉,那么将其移除后就得到一个新的0(A),1(B),2(C),3(E)(注:这里的编号3其实就是原来的4号服务器)服务器列表,此时用户来取数据,同样hash值为12,rehash后的得到的机器编号12%4=0号服务器,可见,此时用户到0号服务器去找数据明显就找不到,出现了cache不命中现象,如果不命中此时应用会从后台数据库重新读取数据再cache到0号服务器上,如果大量用户出现这种情况,那么后果不堪设想。同样,增加一台缓存服务器,也会导致同样的后果。
可以有一种设想,要提高命中率就得减少增加或者移除服务器rehash带来的影响,那么有这样一种算法么?Consistent hashing算法就是这样一种hash算法,简单的说,在移除/添加一个 cache 时,它能够尽可能小的改变已存在 key 映射关系,尽可能的满足单调性的要求。
使用虚拟节点的思想,为每个物理节点(服务器)在圆上分配100~200个点。这样就能抑制分布不均匀,最大限度地减小服务器增减时的缓存重新分布。用户数据映射在虚拟节点上,就表示用户数据真正存储位置是在该虚拟节点代表的实际物理服务器上。
下面有一个图描述了需要为每台物理服务器增加的虚拟节点。
x轴表示的是需要为每台物理服务器扩展的虚拟节点倍数(scale),y轴是实际物理服务器数,可以看出,当物理服务器的数量很小时,需要更大的虚拟节点,反之则需要更少的节点,从图上可以看出,在物理服务器有10台时,差不多需要为每台服务器增加100~200个虚拟节点才能达到真正的负载均衡。
public class ConsistentHash {
/**
* 哈希函数
*/
private final HashFunction hashFunction;
/**
* 虚拟节点数 , 越大分布越均衡,但越大,在初始化和变更的时候效率差一点。 测试中,设置200基本就均衡了。
*/
private final int numberOfReplicas;
/**
* 环形Hash空间
*/
private final SortedMap circle = new TreeMap();
/**
* @param hashFunction
* ,哈希函数
* @param numberOfReplicas
* ,虚拟服务器系数
* @param nodes
* ,服务器节点
*/
public ConsistentHash(HashFunction hashFunction, int numberOfReplicas,
Collection nodes) {
this.hashFunction = hashFunction;
this.numberOfReplicas = numberOfReplicas;
for (T node : nodes) {
this.addNode(node);
}
}
/**
* 添加物理节点,每个node 会产生numberOfReplicas个虚拟节点,这些虚拟节点对应的实际节点是node
*/
public void addNode(T node) {
for (int i = 0; i < numberOfReplicas; i++) {
int hashValue = hashFunction.hash(node.toString() + i);
circle.put(hashValue, node);
}
}
/**移除物理节点,将node产生的numberOfReplicas个虚拟节点全部移除
* @param node
*/
public void removeNode(T node) {
for (int i = 0; i < numberOfReplicas; i++) {
int hashValue = hashFunction.hash(node.toString() + i);
circle.remove(hashValue);
}
}
/**
* 得到映射的物理节点
*
* @param key
* @return
*/
public T getNode(Object key) {
if (circle.isEmpty()) {
return null;
}
int hashValue = hashFunction.hash(key);
// System.out.println("key---" + key + " : hash---" + hash);
if (!circle.containsKey(hashValue)) {
// 返回键大于或等于hash的node,即沿环的顺时针找到一个虚拟节点
SortedMap tailMap = circle.tailMap(hashValue);
// System.out.println(tailMap);
// System.out.println(circle.firstKey());
hashValue = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
// System.out.println("hash---: " + hash);
return circle.get(hashValue);
}
static class HashFunction {
/**
* MurMurHash算法,是非加密HASH算法,性能很高,
* 比传统的CRC32,MD5,SHA-1(这两个算法都是加密HASH算法,复杂度本身就很高,带来的性能上的损害也不可避免)
* 等HASH算法要快很多,而且据说这个算法的碰撞率很低. http://murmurhash.googlepages.com/
*/
int hash(Object key) {
ByteBuffer buf = ByteBuffer.wrap(key.toString().getBytes());
int seed = 0x1234ABCD;
ByteOrder byteOrder = buf.order();
buf.order(ByteOrder.LITTLE_ENDIAN);
long m = 0xc6a4a7935bd1e995L;
int r = 47;
long h = seed ^ (buf.remaining() * m);
long k;
while (buf.remaining() >= 8) {
k = buf.getLong();
k *= m;
k ^= k >>> r;
k *= m;
h ^= k;
h *= m;
}
if (buf.remaining() > 0) {
ByteBuffer finish = ByteBuffer.allocate(8).order(
ByteOrder.LITTLE_ENDIAN);
finish.put(buf).rewind();
h ^= finish.getLong();
h *= m;
}
h ^= h >>> r;
h *= m;
h ^= h >>> r;
buf.order(byteOrder);
return (int) h;
}
}
}
public class Test {
public static void main(String[] args) {
HashSet serverNode = new HashSet();
serverNode.add("127.1.1.1#A");
serverNode.add("127.2.2.2#B");
serverNode.add("127.3.3.3#C");
serverNode.add("127.4.4.4#D");
Map serverNodeMap = new HashMap();
ConsistentHash consistentHash = new ConsistentHash(
new HashFunction(), 200, serverNode);
int count = 50000;
for (int i = 0; i < count; i++) {
String serverNodeName = consistentHash.getNode(i);
// System.out.println(i + " 映射到物理节点---" + serverNodeName);
if (serverNodeMap.containsKey(serverNodeName)) {
serverNodeMap.put(serverNodeName,
serverNodeMap.get(serverNodeName) + 1);
} else {
serverNodeMap.put(serverNodeName, 1);
}
}
// System.out.println(serverNodeMap);
showServer(serverNodeMap);
serverNodeMap.clear();
consistentHash.removeNode("127.1.1.1#A");
System.out.println("-------------------- remove 127.1.1.1#A");
for (int i = 0; i < count; i++) {
String serverNodeName = consistentHash.getNode(i);
// System.out.println(i + " 映射到物理节点---" + serverNodeName);
if (serverNodeMap.containsKey(serverNodeName)) {
serverNodeMap.put(serverNodeName,
serverNodeMap.get(serverNodeName) + 1);
} else {
serverNodeMap.put(serverNodeName, 1);
}
}
showServer(serverNodeMap);
serverNodeMap.clear();
consistentHash.addNode("127.5.5.5#E");
System.out.println("-------------------- add 127.5.5.5#E");
for (int i = 0; i < count; i++) {
String serverNodeName = consistentHash.getNode(i);
// System.out.println(i + " 映射到物理节点---" + serverNodeName);
if (serverNodeMap.containsKey(serverNodeName)) {
serverNodeMap.put(serverNodeName,
serverNodeMap.get(serverNodeName) + 1);
} else {
serverNodeMap.put(serverNodeName, 1);
}
}
showServer(serverNodeMap);
serverNodeMap.clear();
consistentHash.addNode("127.6.6.6#F");
System.out.println("-------------------- add 127.6.6.6#F");
count *= 2;
System.out.println("-------------------- 业务量加倍");
for (int i = 0; i < count; i++) {
String serverNodeName = consistentHash.getNode(i);
// System.out.println(i + " 映射到物理节点---" + serverNodeName);
if (serverNodeMap.containsKey(serverNodeName)) {
serverNodeMap.put(serverNodeName,
serverNodeMap.get(serverNodeName) + 1);
} else {
serverNodeMap.put(serverNodeName, 1);
}
}
showServer(serverNodeMap);
}
/**
* 服务器运行状态
*
* @param map
*/
public static void showServer(Map map) {
for (Entry m : map.entrySet()) {
System.out.println(m.getKey() + ", 存储数据量 " + m.getValue());
}
}
}
运行结果:
127.4.4.4#D, 存储数据量 13177
127.2.2.2#B, 存储数据量 11834
127.3.3.3#C, 存储数据量 12827
127.1.1.1#A, 存储数据量 12162
-------------------- remove 127.1.1.1#A
127.4.4.4#D, 存储数据量 17696
127.2.2.2#B, 存储数据量 15114
127.3.3.3#C, 存储数据量 17190
-------------------- add 127.5.5.5#E
127.4.4.4#D, 存储数据量 12154
127.2.2.2#B, 存储数据量 11878
127.3.3.3#C, 存储数据量 12908
127.5.5.5#E, 存储数据量 13060
-------------------- add 127.6.6.6#F
-------------------- 业务量加倍
127.4.4.4#D, 存储数据量 18420
127.2.2.2#B, 存储数据量 20197
127.6.6.6#F, 存储数据量 21015
127.5.5.5#E, 存储数据量 19038
127.3.3.3#C, 存储数据量 21330