1 Hash 算法应用场景
主要的应用场景归纳起来有两个
- 请求的负载均衡
比如 Nginx 的 ip_hash 可以在客户端 ip 不变的情况下,将其请求始终路由到同一台服务器上,实现了会话粘滞,避免了 session 共享问题。
原理:对 ip 地址或者 sessionId 进⾏计算哈希值,哈希值与服务器数量进⾏取模运算,得到的值就是当前请求应该被路由到的服务器编号。
upstream lagouServer{
ip_hash;
server 111.229.248.243:8080;
server 111.229.248.243:8082;
}
如果没有 ip_hash 策略,那么如何实现会话粘滞?
可以维护⼀张映射表,存储客户端 ip
或者
sessionId
与具体⽬标服务器的映射关系
<ip,tomcat1> 或 <sessionId,tomcat1>
缺点
1
)在客户端很多的情况下,映射表⾮常⼤,浪费内存空间
2
)客户端上下线,⽬标服务器上下线,都需要重新维护映射表,映射表维护成本⼤
-
分布式存储
假设集群中现在有 3 台 Redis 服务器:reids1,redis2,redis3。那么,在进行数据存储时,<key1,value1> 存储到哪个服务器中呢?这时可以采用 hash 算法,对 key 进行 hash 处理。
hash(key1) % 3(服务器数量) = index,index 就是存储该数据的服务器编号。
2 普通 Hash 环算法存在的问题
以 ip_hash 为例,假定客户端 ip 没有发⽣改变,现在 tomcat3 宕机了,服务器数量由 3 个变为了 2 个,之前所有的求模都需要重新计算。
如果在⽣产环境下,后台服务器数量多,客户端也有很多,那么影响是很⼤的。服务器的缩容和扩容都会存在这样的问题,⼤量⽤户的请求会被路由到其他的⽬标服务器,⽤户在原来服务器中的会话都会丢失。
3 一致性 Hash 算法
3.1 基本思路
3.2 手写实现一致性 Hash 算法
(1) 普通 Hash 算法实现
public class GeneralHash {
public static void main(String[] args) {
// 定义客户端IP
String[] clients = new String[]{"10.78.12.3","113.25.63.1","126.12.3.8"};
// 定义服务器数量
int serverCount = 5;// (编号对应0,1,2)
// hash(ip)%node_counts=index
//根据index锁定应该路由到的tomcat服务器
for(String client: clients) {
int hash = Math.abs(client.hashCode());
int index = hash%serverCount;
System.out.println("客户端:" + client + " 被路由到服务器编号为:" + index);
}
}
}
(2) 一致性 Hash 算法实现(不含虚拟节点)
public class ConsistentHashNoVirtual {
public static void main(String[] args) {
//step1 初始化:把服务器节点IP的哈希值对应到哈希环上
// 定义服务器ip
String[] tomcatServers = new String[]{"123.111.0.0", "123.101.3.1", "111.20.35.2", "123.98.26.3"};
SortedMap<Integer, String> hashServerMap = new TreeMap<>();
for (String tomcatServer : tomcatServers) {
// 求出每⼀个ip的hash值,对应到hash环上,存储hash值与ip的对应关系
int serverHash = Math.abs(tomcatServer.hashCode());
// 存储hash值与ip的对应关系
hashServerMap.put(serverHash, tomcatServer);
}
//step2 针对客户端IP求出hash值
// 定义客户端IP
String[] clients = new String[]{"10.78.12.3", "113.25.63.1", "126.12.3.8"};
for (String client : clients) {
int clientHash = Math.abs(client.hashCode());
//step3 针对客户端,找到能够处理当前客户端请求的服务器(哈希环上顺时针最近)
// 根据客户端ip的哈希值去找出哪⼀个服务器节点能够处理()
// tailMap 返回 key 大于或等于 fromKey 的数据
SortedMap<Integer, String> integerStringSortedMap = hashServerMap.tailMap(clientHash);
if (integerStringSortedMap.isEmpty()) {
// 取哈希环上的顺时针第⼀台服务器
Integer firstKey = hashServerMap.firstKey();
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
} else {
Integer firstKey = integerStringSortedMap.firstKey();
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
}
}
}
}
(3) 一致性 Hash 算法实现(含虚拟节点)
public class ConsistentHashWithVirtual {
public static void main(String[] args) {
//step1 初始化:把服务器节点IP的哈希值对应到哈希环上
// 定义服务器ip
String[] tomcatServers = new String[]{"123.111.0.0", "123.101.3.1", "111.20.35.2", "123.98.26.3"};
SortedMap<Integer, String> hashServerMap = new TreeMap<>();
// 定义针对每个真实服务器虚拟出来⼏个节点
int virtaulCount = 3;
for (String tomcatServer : tomcatServers) {
// 求出每⼀个ip的hash值,对应到hash环上,存储hash值与ip的对应关系
int serverHash = Math.abs(tomcatServer.hashCode());
// 存储hash值与ip的对应关系
hashServerMap.put(serverHash, tomcatServer);
// 处理虚拟节点
for (int i = 0; i < virtaulCount; i++) {
int virtualHash = Math.abs((tomcatServer + "#" + i).hashCode());
hashServerMap.put(virtualHash, "----由虚拟节点" + i + "映射过来的请求:" + tomcatServer);
}
}
//step2 针对客户端IP求出hash值
// 定义客户端IP
String[] clients = new String[]{"10.78.12.3", "113.25.63.1", "126.12.3.8"};
for (String client : clients) {
int clientHash = Math.abs(client.hashCode());
//step3 针对客户端,找到能够处理当前客户端请求的服务器(哈希环上顺时针最近)
// 根据客户端ip的哈希值去找出哪⼀个服务器节点能够处理()
SortedMap<Integer, String> integerStringSortedMap = hashServerMap.tailMap(clientHash);
if (integerStringSortedMap.isEmpty()) {
// 取哈希环上的顺时针第⼀台服务器
Integer firstKey = hashServerMap.firstKey();
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
} else {
Integer firstKey = integerStringSortedMap.firstKey();
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
}
}
}
}