一致性hash算法讲解
hash
hash(哈希):即是将一个大的集合中的元素映射到一个特定大小的集合中的操作。
普通hash的弊端
普通的hash一般采用求余的方式得到映射关系,在java中hashmap中的hash索引的策略就是hashcode % table.size()-->实际为 : hashcode & (table.size() - 1)
,但是这个是在节点的数量一定的情况下,在redis中的,redis主要是做缓存用的,如果一个redis集群需要增加或者删除一个节点,那么采用这种普通的hash策略会造成大量的key不能够命中,进而产生缓存失效,大量的请求到达数据库,如果数据库没有做高可用处理,那么在大量的请求到数据库时,很容易就将数据库服务打崩。
举个例子:
一个含有3个节点的redis集群,现在需要增加一个新的redis节点,在这种情况下,会造成75%的缓存失效,进而会有75%的请求直接打到数据库,随着集群节点的数量不断的增大,在原有的集群的基础之上,添加一个新的节点,会造成更大比例的缓存失效。
原始集群节点数量 m ,失效缓存比例:m / (m + 1);
一致性hash原理
一致性hash是将映射集合规定为一个0~2^32-1的范围的环,收尾相连。
node和key分别按照hash函数计算,最后得到一个值,在环上会有一个位置与之对应,如图,有三个node和三个key在环上,每一个key从当前位置出发顺时针寻找离它最近的一个node,作为最终的node,环上的其他key节点按照同样的方式寻找与之对应的node。
但是这里还有一个问题,就是每个node的位置不一定是均匀地分布在环上的,如果不均匀,那么就会有某个节点承担的请求缓存压力相比较于其他节点要多,所以为了均衡各个节点的压力,node节点应当尽量均匀地分布在环上,为此,引入一个虚拟节点的概率,即是,每一个物理节点会有多个与之对应的虚拟节点, 然后让虚拟节点在环上均匀的分布,如此来达到物理节点巧妙地分布在环上,解决物理节点在环上分布不均匀的问题。
代码实现
public class ConsistentHash {
private int virNodesNum;
private Map<String, List<Integer>> realNodeToVirNodesHashCode = new ConcurrentHashMap<>();
private SortedMap<Integer, String> ring = new TreeMap<>();
public ConsistentHash(int virNodesNum) {
this.virNodesNum = virNodesNum;
}
public synchronized void addNode(String node) {
if (realNodeToVirNodesHashCode.containsKey(node))
throw new RuntimeException(node + " has already in the pool.");
// 创建虚拟节点,并放置到环上去
// 虚拟节点的hashcode 放入到 realNodeToVirNodesHashCode 中
int hash;
List<Integer> hashCodes = new ArrayList<>();
for (int i = 0; i < this.virNodesNum; i++) {
hash = rehash(node + "---" + i);
ring.putIfAbsent(hash, node);
hashCodes.add(hash);
}
realNodeToVirNodesHashCode.put(node, hashCodes);
}
public synchronized void removeNode(String node) {
if (!realNodeToVirNodesHashCode.containsKey(node))
throw new RuntimeException(node + " is not in the pool.");
// 得到该节点对应的虚拟节点hashcode列表
List<Integer> virNodes = realNodeToVirNodesHashCode.get(node);
// 从环上依个删除
for (Integer virNode : virNodes) {
ring.remove(virNode);
}
realNodeToVirNodesHashCode.remove(node);
}
public String getNode(String key) {
int hash = rehash(key);
// 从环上选择一个顺时针方向
// (升序方向--或者降序方向都可,这里升序)的
// 第一个虚拟节点作为目标节点返回
SortedMap<Integer, String> tailMap = ring.tailMap(hash); // tailMap中的所有key值>=hash
return tailMap.isEmpty() ? ring.get(ring.firstKey()) : tailMap.get(tailMap.firstKey());
}
private int rehash(Object o) {
int hash = o.hashCode();
hash *= 16777619;// 32 bit FNV_prime = 2^24 + 2^8 + 0x93 = 16777619
return Math.abs(hash ^ (hash >>> 16));
}
public static void main(String[] args) {
ConsistentHash consistentHash = new ConsistentHash(150);
consistentHash.addNode("192.168.10.10");
consistentHash.addNode("192.168.10.11");
consistentHash.addNode("192.168.10.12");
consistentHash.addNode("192.168.10.13");
String [] tmp = new String[1000000];
for (int i = 0; i < 1000000; i++) {
tmp[i] = consistentHash.getNode(String.valueOf(i));
}
consistentHash.addNode("192.168.10.14");
int cnt = 0;
for (int i = 0; i < 1000000; i++) {
if (tmp[i].equals(consistentHash.getNode(String.valueOf(i)))) cnt++;
tmp[i] = consistentHash.getNode(String.valueOf(i));
}
System.out.println("添加一个节点后命中率:" + cnt *1.0 / 1000000 * 100 + "%");
consistentHash.removeNode("192.168.10.12");
cnt = 0;
for (int i = 0; i < 1000000; i++) {
if (tmp[i].equals(consistentHash.getNode(String.valueOf(i)))) cnt++;
}
System.out.println("移除一个节点后命中率:" + cnt *1.0 / 1000000 * 100 + "%");
}
}