一致性哈希
-哈希取模是我们经常使用的负载均衡策略,但是这种策略有一个明显的弊端,当服务扩容或缩容时,大部分key都需要进行移动,当服务器资源不断增长,迁移必定是灾难性的。于是一致性哈希算法孕育而生
原理
-将服务端分片名称进行hash处理并将处理后的值打在一个大小为2^32的闭环圆上,记圆上的点为x
-客户端节点名称同上操作,记圆上的点为y
-y进行顺时针旋转,找到离自己最近的x(闭环圆一定能找到x),x即为客户端要找的服务器分片
问题点
-假设服务器分片较少且哈希分布不均匀,怎么办?
-有哪些哈希算法适用于一致性哈希?
-服务器分片个数变化后,如何快速找到受影响的key并进行key的迁移?
String hashCode源码分析
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
关键代码: h = 31 * h + val[i];
直观的看不太容易理解,这里举个例子:字符串”abc”公式转换:
hash = (31*0+a)*31 + b)*31 + c = a*31^2 + b*32 + c;(拆解方式便于理解,并不是数学公式推导)
到这里相信大家已经推敲出计算公式了:hash = [0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]
那么为什么是31呢?原因其实很简单:31只能被1整除且基数够大,不容易产生hash冲突。
从源码分析String hashCode并不适用,因为如果两个字符串高位值相同,低位值相似的话,hash出来的值则非常接近,一致性Hash值离散度低
比如 ip:”192.168.1.131” 和ip:”192.168.1.131” 对应的hashcode分别是:-2051273165和-2051273195
这里抛一个问题,大家可以思考一下:int类型数值如果乘法溢出了会怎样呢?
代码实现
import com.xx.registry.util.Md5Util;
import javax.validation.constraints.Null;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* @Auther:
* @Despriction: 一致性hash算法实现, hash固长2^32
* @Date:Created by _chaoziliang
* @Modify By:
*/
public class UniformityHash {
private SortedMap<Integer, String> hashMap;//
private int fictitious = 0;//虚拟节点个数
/**
* Init
*
* @param nodes 节点名称
* @param fictitious 虚拟节点个数
*/
public UniformityHash(String[] nodes, final int fictitious) {
hashMap = new TreeMap<>();
this.fictitious = fictitious;
for (String node : nodes) {
addNode(node);
}
}
/**
* 从map中找到匹配的节点
*
* @param matchNode 匹配节点
*/
public String getMatchNode(String matchNode) {
//获取大于此hash值节点往下的数据
SortedMap<Integer, String> tailMap = hashMap.tailMap(getHashCode(matchNode));
//○闭环
if (tailMap.size() == 0) {
return hashMap.get(hashMap.lastKey());
}
return tailMap.get(tailMap.firstKey());
}
/**
* 创建节点
*
* @param node 节点名称
*/
public void addNode(String node) {
for (int i = 0; i <= fictitious; i++) {
hashMap.put(getHashCode(node + i), node);
}
}
/**
* 删除节点
*
* @param node 匹配节点
*/
public void removeNode(String node) {
for (int i = 0; i <= fictitious; i++) {
hashMap.remove(getHashCode(node + i));
}
}
/**
* 一致性hash,md5方式
*
* @param node 节点名称
*/
private int getHashCode(String node) {
int hashResult = -1;
try {
String md5Str = Md5Util.encode(node);
char[] md5Chars = md5Str.toCharArray();
hashResult = 0;
for (int i = 0, j = md5Chars.length; i < j; i++) {
hashResult += ((md5Chars[i] & 1) << (i + 1));
}
return hashResult;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return hashResult;
}
/**
* 留一个问题:如何计算扩容、缩容后,变化key前后的node
*
* @param args
*/
public static void main(String[] args) {
Map<String,Null> compareMap = new HashMap<>();//kv
int change = 0;
//
String p_appr = "192.168.1.";
String c_appr = "172.168.9.";
String[] ips = {p_appr + "111", p_appr + "112", p_appr + "113", p_appr + "114", p_appr + "115", p_appr + "116", p_appr + "117"};
UniformityHash uniformity = new UniformityHash(ips, 50);
System.out.println("增加节点前==============");
for (int i = 1; i < 5000; i++) {
String mapping = c_appr + i+"=>"+ uniformity.getMatchNode(c_appr + i);
compareMap.put(mapping,null);
}
System.out.println("ips增加节点前存储个数。。懒的写了");
System.out.println("增加节点后==============");
uniformity.addNode(p_appr+"18");
for (int i = 1; i < 5000; i++) {
String mapping = c_appr + i+"=>"+ uniformity.getMatchNode(c_appr + i);
if(!compareMap.containsKey(mapping)){
change++;
}
}
System.out.println("ips增加节点后存储个数。。懒的写了");
System.out.println("增加节点后,key值需迁移数:"+change);
}}
控制台结果输出
增加节点前==============
ips增加节点前存储个数。。懒的写了
增加节点后==============
ips增加节点后存储个数。。懒的写了
增加节点后,key值需迁移数:604