目录
一. 分布式与集群概念回顾
参考:分布式系统架构解决方案—Dubbo(介绍)_舞鹤白沙编码日志-CSDN博客
分布式和集群是不⼀样的,
分布式⼀定是集群,但是集群不⼀定是分布式
(因为集群就是多个实例⼀起⼯作,分布式将⼀个系统拆分之后那就是多个实例;集群并不⼀定是分布式,因为复制型的集群不是拆
分⽽是复制)

二. Hash算法介绍
Hash
算法,⽐如说在安全加密领域
MD5
、
SHA
等加密算法,在数据存储和查找⽅⾯有
Hash
表等
,
以上都应⽤到了
Hash
算法。
为什么需要使⽤
Hash?
Hash
算法较多的应⽤在数据存储和查找领域,最经典的就是
Hash
表,它的查询效率⾮常之⾼,其中的哈希算法如果设计的⽐较
ok
的话,那么
Hash
表的数据查询时间复杂度可以接近于
O(1)
,示例:
需求:提供⼀组数据
1,5,7,6,3,4,8
,对这组数据进⾏存储,然后随便给定⼀个数
n
,请你判断
n
是否存在于刚才的数据集中?
list:List[1,5,7,6,3,4,8]// 通过循环判断来实现for(int element: list) {if(element == n) {如果相等,说明 n 存在于数据集中}}
以上这种⽅法叫做
顺序查找法
:这种⽅式我们是通过循环来完成,⽐较原始,效率也不⾼
⼆分查找:
排序之后折半查找,相对于顺序查找法会提⾼⼀些效率,但是效率也并不是特别好
我能否不循环!不⼆分!⽽是通过⼀次查询就把数据
n
从数据集中查询出来???可以!

定义⼀个数组,数组⻓度⼤于等于数据集⻓度,此处⻓度为
9
,数据
1
就存储在下标为
1
的位置,
3
就存储在下标为
3
的元素位置,,,依次类推。
这个时候,我想看下
5
存在与否,只需要判断
list.get(5) array[5]
是否为空,如果为空,代表
5
不存在于数据集,如果不为空代表
5
在数据集当中,通过⼀次查找就达到了⽬的,时间复杂度为
O(1)
。
这种⽅式叫做
“
直接寻址法
”
:直接把数据和数组的下标绑定到⼀起,查找的时候,直接
array[n]
就取出了数据
优点:速度快,⼀次查找得到结果
缺点:
1
)浪费空间,⽐如
1,5,7,6,3,4,8,12306
,最⼤值
12306
,按照上述⽅式需要定义⼀个⽐如⻓度为
12307
的数组,但是只存储零星的⼏个数据,其他位置空间都浪费着
2
)数据如:
1,5,7,6,3,4,8,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2
最⼤值
12
,⽐如开辟
13
个空间,存储不了这么多内容
现在,换⼀种设计,如果数据是
3
,
5
,
7
,
12306
,⼀共
4
个数据,我们开辟任意个空间,⽐如
5
个,那么具体数据存储到哪个位置呢,我们可以对数据进⾏求模(对空间位置数
5
),根据求模余数确定存储
位置的下标,⽐如
3%5=3
,就可以把
3
这个数据放到下标为
3
的位置上,
12306%5=1
,就把
12306
这个
数据存储到下标为
1
的位置上

上⾯对数据求模 (数据
%
空间位置数) 他就是⼀个
hash
算法,只不过这是⼀种⽐较普通⼜简单的
hash算法,这种构造
Hash
算法的⽅式叫做除留余数法.
如果数据是
1
,
6
,
7
,
8
,把这
4
个数据存储到上⾯的数组中

在此基础上采⽤开放寻址法(了解)
开放寻址法:
1
放进去了,
6
再来的时候,向前或者向后找空闲位置存放,不好的地⽅,如果数组⻓度定义好了⽐如
10
,⻓度不能扩展,来了
11
个数据,不管
Hash
冲突不冲突,肯定存不下这么多数据。
拉链法
:数据⻓度定义好了,怎么存储更多内容呢,算好
Hash
值,在数组元素存储位置放了⼀个链表。

如果
Hash
算法设计的⽐较好的话,那么查询效率会更接近于
O(1)
,如果
Hash
算法设计的⽐较
low
,那么查询效率就会很低了。

所以,
Hash
表的查询效率⾼不⾼取决于
Hash
算法,
hash
算法能够让数据平均分布,既能够节省空间⼜能提⾼查询效率。
Hash
算法的研究是很深的⼀⻔学问,⽐较复杂,⻓久以来,
Hash
表内部的
Hash
算法
也⼀直在更新,很多数学家也在研究。
除留余数法
3%5
线性构造
Hash
算法
直接寻址法也是⼀种构造
Hash
的⽅式,只不过更简单,表达式:
H(key)=key
⽐如
H(key)=a*key + b(a,b
是常量
)
hashcode
其实也是通过⼀个
Hash
算法得来的。
三. Hash算法应⽤场景
Hash算法在分布式集群架构中的应⽤场景
Hash
算法在很多分布式集群产品中都有应⽤,⽐如分布式集群架构
Redis
、
Hadoop
、
ElasticSearch
,Mysql
分库分表,
Nginx
负载均衡等
主要的应⽤场景归纳起来两个
(一)请求的负载均衡(⽐如nginx的ip_hash策略)
Nginx
的
IP_hash
策略可以在客户端
ip
不变的情况下,将其发出的请求始终路由到同⼀个⽬标服务
器上,实现会话粘滞,避免处理
session
共享问题
如果没有
IP_hash
策略,那么如何实现会话粘滞?
可以维护⼀张映射表,存储客户端
IP
或者
sessionid
与具体⽬标服务器的映射关系
<ip,tomcat1>
缺点 :
1
)那么,在客户端很多的情况下,映射表⾮常⼤,浪费内存空间
2
)客户端上下线,⽬标服务器上下线,都会导致重新维护映射表,映射表维护成本很⼤
如果使⽤哈希算法,事情就简单很多,我们可以对
ip
地址或者
sessionid
进⾏计算哈希值,哈希值与服务器数量进⾏取模运算,得到的值就是当前请求应该被路由到的服务器编号,如此,同⼀个客户端
ip
发送
过来的请求就可以路由到同⼀个⽬标服务器,实现会话粘滞。
(二)分布式存储
以分布式内存数据库
Redis
为例
,
集群中有
redis1
,
redis2
,
redis3
三台
Redis
服务器
那么
,
在进⾏数据存储时
,<key1,value1>
数据存储到哪个服务器当中呢?针对
key
进⾏
hash
处理
hash(key1)%3=index,
使⽤余数
index
锁定存储的具体服务器节点
三. 普通Hash算法存在的问题
普通
Hash
算法存在⼀个问题,以
ip_hash
为例,假定下载⽤户
ip
固定没有发⽣改变,现在
tomcat3
出现了问题,
down
机了,服务器数量由
3
个变为了
2
个,之前所有的求模都需要重新计算。
如果在真实⽣产情况下,后台服务器很多台,客户端也有很多,那么影响是很⼤的,缩容和扩容都会存在这样的问题,⼤量⽤户的请求会被路由到其他的⽬标服务器处理,⽤户在原来服务器中的会话都会丢
失。
四. ⼀致性Hash算法
⼀致性哈希算法思路如下:
⾸先有⼀条直线,直线开头和结尾分别定为为
1
和
2
的
32
次⽅减
1
,这相当于⼀个地址,对于这样⼀条线,弯过来构成⼀个圆环形成闭环,这样的⼀个圆环称为
hash
环。我们把服务器的
ip
或者主机名求
hash
值然后对应到
hash
环上,那么针对客户端⽤户,也根据它的
ip
进⾏
hash
求值,对应到环上某个位
置,然后如何确定⼀个客户端路由到哪个服务器处理呢?按照顺时针⽅向找最近的服务器节点。

假如将服务器
3
下线,服务器
3
下线后,原来路由到
3
的客户端重新路由到服务器
4
,对于其他客户端没有影响只是这⼀⼩部分受影响(请求的迁移达到了最⼩,这样的算法对分布式集群来说⾮常合适的,避免
了⼤量请求迁移 )

增加服务器
5
之后,原来路由到
3
的部分客户端路由到新增服务器
5
上,对于其他客户端没有影响只是这⼀⼩部分受影响(请求的迁移达到了最⼩,这样的算法对分布式集群来说⾮常合适的,避免了⼤量请求
迁移
)

1
)如前所述,每⼀台服务器负责⼀段,⼀致性哈希算法对于节点的增减都只需重定位环空间中的⼀⼩部分数据,具有较好的容错性和可扩展性。
但是
,⼀致性哈希算法在服务节点太少时,容易因为节点分部不均匀⽽造成数据倾斜问题。例如系统中只有两台服务器,其环分布如下,节点
2
只能负责⾮常⼩的⼀段,⼤量的客户端。
请求落在了节点
1
上,这就是
数据(请求)倾斜问题
2
)为了解决这种数据倾斜问题,⼀致性哈希算法引⼊了虚拟节点机制,即对每⼀个服务节点计算多个哈希,每个计算结果位置都放置⼀个此服务节点,称为虚拟节点。
具体做法可以在服务器
ip
或主机名的后⾯增加编号来实现。⽐如,可以为每台服务器计算三个虚拟节点,于是可以分别计算
“
节点
1
的
ip#1”
、
“
节点
1
的
ip#2”
、
“
节点
1
的
ip#3”
、
“
节点
2
的
ip#1”
、
“
节点
2
的
ip#2”
、
“
节点
2
的
ip#3”
的哈希值,于是形成六个虚拟节点,当客户端被路由到虚拟节点的时候其实是被
路由到该虚拟节点所对应的真实节点。

五. 手写实现一致性Hash算法
(一)普通Hash算法实现
/**
* 普通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) {
//Math.abs为取绝对值
int hash = Math.abs(client.hashCode());
int index = hash%serverCount;
System.out.println("客户端:" + client + " 被路由到服务器编号为:" + index);
}
}
}
(二)⼀致性Hash算法实现(不含虚拟节点)
import java.util.SortedMap;
import java.util.TreeMap;
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为可排序的Map
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的哈希值去找出哪一个服务器节点能够处理()
//SorteMap当中有tailMap这个方法,该方法参数传入一个Key值进去,可以返回大于这个key值的所有key以及对应的value组成的map,
//我们只需要取出返回的这个MAP中的第一个key所对应的value,这个value就代表离我们传入的key顺时针最近的服务器。
SortedMap<Integer, String> integerStringSortedMap = hashServerMap.tailMap(clientHash);
if(integerStringSortedMap.isEmpty()) {
// 取哈希环上的顺时针第一台服务器,hashServerMap代表哈希环,也就是按key值(服务器IP的hashCode)从小到大存储的服务器IP。
Integer firstKey = hashServerMap.firstKey();
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
}else{
//integerStringSortedMap为比客户端ip的hashCode大的所有的服务器IP的map
Integer firstKey = integerStringSortedMap.firstKey();
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
}
}
}
}
(三)⼀致性Hash算法实现(含虚拟节点)
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
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));
}
}
}
}
六. Nginx 配置⼀致性Hash负载均衡策略
ngx_http_upstream_consistent_hash
模块是⼀个负载均衡器,使⽤⼀个内部⼀致性
hash
算法来选择合适的后端节点。
该模块可以根据配置参数采取不同的⽅式将请求均匀映射到后端机器,
consistent_hash $remote_addr
:可以根据客户端
ip
映射
consistent_hash $request_uri
:根据客户端请求的
uri
映射
consistent_hash $args
:根据客户端携带的参数进⾏映
ngx_http_upstream_consistent_hash
模块是⼀个第三⽅模块,需要我们下载安装后使⽤
1
)
github
下载
nginx
⼀致性
hash
负载均衡模块
https://github.com/replay/ngx_http_consistent_hash

2
)将下载的压缩包上传到
nginx
服务器,并解压
3
)我们已经编译安装过
nginx
,此时进⼊当时
nginx
的源码⽬录,执⾏如下命令
./confifigure --add-module=/root/ngx_http_consistent_hash-master
make
make install
4
)
Nginx
就可以使⽤啦,在
nginx.conf
⽂件中配置
