一致性哈希算法
场景如下:
- 有三台缓存服务器分别为A、B、C,编号依次为1、2、3,现在有N张名称不重复的图片平均分配到每台服务器上进行缓存,如何设计一套算法,使得每次图片请求都能命中该图片所在的缓存服务器呢?
- 传统的做法是,得到每张图片的名字的哈希值,然后对服务器数量进行取模,得到的值就是对应的服务器编号。
- 如:现在有一张图片为a.jpg,通过hash(a.jpg)得到一个哈希值9,因为我们有三台服务器,就用9对3进行取模运算得到0,那么就将a.jpg缓存到A这台服务器上,每次请求a.jpg的时候就通过同样的算法知道该图片缓存在哪台服务器上了。
那么问题来了,如果现在要添加或者删除N台服务器会怎么样呢?
我们以添加一台服务器D为例,其编号为4,在原有hash方法不变的情况下,a.jpg对应的哈希值仍然是9,但此时我们有4台服务器了,所以需要用9对4进行取模,得到1,也就是对应服务器B,而我们知道a.jpg是缓存在服务器A中的,此时缓存将无法命中。
当缓存服务器数量发生变化时,几乎所有请求指向的缓存服务器都会发生改变,会引起缓存的雪崩,可能会引起整体系统压力过大而崩溃(大量缓存同一时间失效)。
解决以上问题的方案就是一致性哈希算法
什么是一致性哈希算法
一致性哈希算法在1997年由麻省理工学院提出,是一种特殊的哈希算法,目的是解决分布式缓存的问题。 [1] 在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。一致性哈希解决了简单哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的动态伸缩等问题 [2] 。
说人话就是,一致性哈希算法也是通过取模的方式找到请求对应的服务器,只不过,它是以 2^32取模。
我们假设现在有一个圆环,将这个圆环均分 2^32 份,那么它上面便有 2^32 个点,所以每个请求对应的hash值取模的结果必然是在[0,2^32]这个区间上的一个点。
我们在这个环上取三个点对应三台服务器A、B、C,其中C到A的区间为是s1,A到B的区间为s2,B到C的区间为s3。此时若有一个点落在s1圆弧上,则通过顺时针方向找到最近的服务器就是A,至此,一致性哈希基本完成。
但是上诉一致性哈希算法依然有一个缺陷,那就是每个请求对应的哈希值分布的区间并非是均匀的,就会出现某个或几个服务器负载会异常高的情况。
所以便引入了虚拟节点的概念,每台服务器对应多个虚拟节点,也就对应环上多个点,N个虚拟节点将服务器分成了N个区间,这样使得每个请求落在各个服务器的概率趋于平均。
源码如下:
//物理节点集合 用String 类型表示
private List<String> physicalIps = new ArrayList<>();
//每个物理ip对应实现的虚拟节点数
private Map<String, List<Integer>> physicalIp2Virtuals = new HashMap<>();
//每个物理ip分配的虚拟节点数量,默认0
private int virtualsNum;
//虚拟节点对应的物理节点 相当于环 用TreeMap实现红黑树存储
private SortedMap<Integer, String> sortedMap = new TreeMap<>();
public ConsistencyHash(int virtualsNum) {
this.virtualsNum = virtualsNum;
}
public ConsistencyHash() {
}
/**
* 增加物理ip 到环
*/
public void addServer(String physicalIp) {
this.physicalIps.add(physicalIp);
//加入物理ip对应的虚拟集合
ArrayList<Integer> virtuals = new ArrayList<>();
this.physicalIp2Virtuals.put(physicalIp, virtuals);
int count = 0, i = 0;
while (count < this.virtualsNum) {
i++;
int hash = getHash(physicalIp+"&&v-"+i);
//解决hash碰撞问题
if (!sortedMap.containsKey(hash)) {
virtuals.add(hash);
this.sortedMap.put(hash, physicalIp);
count ++;
}
// System.out.println(count);
}
}
/**
* 获取物理ip
*/
public String getServer(String key){
int hash = getHash(key);
//获取大于环上大于key hash的所有虚拟对应 物理ip
SortedMap<Integer, String> integerStringSortedMap = this.sortedMap.tailMap(hash);
if (!integerStringSortedMap.isEmpty()){
return integerStringSortedMap.get(integerStringSortedMap.firstKey());
}else { //没有数据时 取第一个虚拟节点上的 物理ip 顺时针取值
return this.sortedMap.get(sortedMap.firstKey());
}
}
/**
* 移除物理ip
*/
public void removeServer(String physicalIp){
//获得此物理ip 对应所有虚拟节点
List<Integer> integers = this.physicalIp2Virtuals.get(physicalIp);
if (!integers.isEmpty()) {
for (Integer integer : integers) {
this.sortedMap.remove(integer);
}
}
this.physicalIps.remove(physicalIp);
this.physicalIp2Virtuals.remove(physicalIp);
}
//计算hash值
public static int getHash(String str) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < str.length(); i++)
hash = (hash ^ str.charAt(i)) * p;
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
// 如果算出来的值为负数则取其绝对值
if (hash < 0)
hash = Math.abs(hash);
return hash;
}
还没有评论,来说两句吧...