目录
- 1.回顾hash函数
- 1.1 hash函数的特点
- 1.2 配合代码介绍hash函数的实现
- 2.负载均衡
- 2.1 传统的负载均衡
- 2.2 一致性hash原理
- 2.2.1 建立hash环
- 2.2.2 虚拟节点技术
1.回顾hash函数
- hash函数中文名就叫哈希函数
1.1 hash函数的特点
- 1)hash函数的输入域是无穷大的,你可以对你的hash函数输入任意的内容
- 2)hash函数的输出域可能很大,但是是有穷的。因为对于hash值的长度是有规定的,而hash数可以看做是一串16进制数字,长度规定的十六进制一定会有一个上限
- 3)hash函数不是随机过程,没有任何随机成分,输入相同,输出即相同。确定的输入导致确定的输出。
- hash过程是一个通过一定模板化的计算过程得到的结果,所以不是一个随机数
- 4)因为输入无穷大而输出有限,所以一定存在不同输入的相同输出【但是不会存在相同输入不同输出】,这种现象叫做hash碰撞,或者说散列冲突。
- 5)不同的输入可能非常的像,但是不管输入多像,hash值也会是天差地别【除非发生hash碰撞】,也可能比较相近,反正hash结果一定是十分离散的。比如:
- 6)hash函数输出结果是有均匀性的,当然这建立在很多的输入上【百万级别?】,但是某一段的数据量和其他等长度的段比较是均匀近似相等的。
- 7)hash函数均匀的输出结果去对m求余之后,结果会0-(m-1)上十分均匀
# 首先我们要知道求余的规律
n % m =?
n是任意数,那么结果一定是0到(m-1)
举例 任意数 % 1 : 结果从0到1-1
任意数 % 2 : 结果从0到2-1
.......................
# 然后因为我们得到的hash值是分布均匀的,
# 那么求余之后在0-(m-1)线段上分布的结果也是均匀的
# 看下图
8)hash函数是单向函数,拥有了hash值是无法转为原数据的,因为不同的数据可以有相同的hash值。
1.2 配合代码介绍hash函数的实现
// 这是在maven里实现的
// 可以把下面的@Test方法改为main方法就是正常的java写法
import org.junit.Test;
import javax.xml.bind.DatatypeConverter;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
// 因为hash的算法多种多样,所以对于原理的了解远远重要于算法实现
// 至于具体算法,根据具体公司的具体业务需求才能获得具体的算法
// 定义一个hash类用来管理hash函数的调用
public static class Hash{
private MessageDigest hash;
// 我们定义的hash类的构造方法,要求用户提供一个算法类型
// 然后通过MessageDigest.getInstance方法去创建带有该算法的MessageDigest对象用来后面求hashCoe
public Hash(String algorithm){
try{
hash = MessageDigest.getInstance(algorithm);
}catch (NoSuchAlgorithmException e){
e.printStackTrace();
}
}
//用户在创建对象并传入算法类型后要调用该方法才能得到hash值
public String hashCode(String input){
//java内置包的方法
return DatatypeConverter.printHexBinary(hash.digest(input.getBytes())).toUpperCase();
}
}
//hash函数测试
@Test
public void testHash(){
// 先看java自带的包里有多少种用来计算hash值的算法
System.out.println("支持的算法有:");
//algorithms是算法的意思
for(String str : Security.getAlgorithms("MessageDigest")){
System.out.print(str+" ");
}
System.out.println();
System.out.println("==============================================================================");
// 我们使用其中一种“SHA-256”的算法
String algorithm = "SHA-256";
Hash hash = new Hash(algorithm);
// 这里我们尝试相同的输入看看是什么输出结果
String input0 = "zhangfenghuazhangfenghua1";
String input1 = "zhangfenghuazhangfenghua1";
String input2 = "zhangfenghuazhangfenghua2";
String input3 = "zhangfenghuazhangfenghua3";
String input4 = "zhangfenghuazhangfenghua4";
String input5 = "zhangfenghuazhangfenghua5";
String input6 = "zhangfenghuazhangfenghua6";
String input7 = "zhangfenghuazhangfenghua7";
// 看到上面的input都挺像的,就只有最后一个字符不同,结果会不会相似还是天差地别呢?
System.out.println("input0:"+hash.hashCode(input0));
System.out.println("input1:"+hash.hashCode(input1));
System.out.println("input2:"+hash.hashCode(input2));
System.out.println("input3:"+hash.hashCode(input3));
System.out.println("input4:"+hash.hashCode(input4));
System.out.println("input5:"+hash.hashCode(input5));
System.out.println("input6:"+hash.hashCode(input6));
}
- 我们来看以下结果
- input0和input1的输入是一样的,因此输出也是一样的
支持的算法有:
SHA-384 SHA-224 SHA-256 MD2 SHA SHA-512 MD5
==============================================================================
# 我们可以看到相同的输入出来的hash结果相同,3)特征得以验证
input0:52444F963DA886215108EAC55BE81163B935760441A804044017B3BBF5D1020D
input1:52444F963DA886215108EAC55BE81163B935760441A804044017B3BBF5D1020D
# 我们可以看到相似的输入出来的hash结果天差万别,5)特征得以验证
input2:4C4477FCCF1B654B95E9E285751B09AD94C65EE1B9D2896A0F8771B6091A0515
input3:4C7936772C084B8BB0FACC3B3EE5CF0CAEA523672F9DF016C15EAF1A3C6E1709
input4:D8F51B45B18C6DE326BE314F14FBDB7377FBD735629DD6B1AE86D652B3B2E9E1
input5:1404AB0939620D6041D10CA9C420D299AE892A803981C221120E360A83F1EEBF
input6:E2D3107D9F23AEDD6F6C0A78C7B1D62F7FECE5D1F833B804CA385C52C20CB4B8
2.负载均衡
2.1 传统的负载均衡
- 假设一个存储数据的场景:
* 假设一个存储数据的场景:比如用户输入了(中国,66) 这个键值对数据
* 该数据传到了代理服务器tomcat【后端中的前端】
* tomcat把数据发送给逻辑端【controller控制器、servicer端、dao类】
* 逻辑端把数据存到数据端【数据库】
* 假设我们数据端有3台服务器0,1,2
* 我们对数据中的key进行hash()得到hash值再%3【因为有3台服务器,这样我们就能只得到0或1或2的结果】
* 在很多request请求数据需要存储的时候这三台服务器存储的数据量和承载压力十分均衡。用到hash函数的一个求余后仍均匀原理
逻辑端对数据进行处理到数据端的过程,看图:
- 当然既然说到传统了,那就一定有他的局限和问题:问题可大了
- 一堆数据都是%3才能知道存到哪,如果我们突然增加一台服务器呢?或者减少一台呢?所有数据都是%3,这时候是不是全部数据要重新%4或者%2?这是一个全量程度的操作,十分不友好!
- 这个传统的负载均衡的办法已经被淘汰
2.2 一致性hash原理
2.2.1 建立hash环
- 解决增删服务器代价全量问题
- 我们指定一个环状的数据结构【环链表或者什么的】里面表示hash值从0到(2^64)-1,注意是hash值而不是求余后的结果
- 我们这里也有3台服务器m1,m2,m3。他们作为三个单独的个体一定会有差异化的数据来区分对吧,比如他们分别的ip地址?或者mac物理地址。我们假设三台服务器对应的mac地址数据为a1【70-18-8B-2D-CC-8C】,a2【90-19-86-2D-CC-8C】,a3【80-20-3B-2D-CC-8C】,这些都是字符串,用来求hash值的字符串。
- 那么,对这三台机器的mac地址求hash()值就可以获得3个不相同的hash值,并且这三个hash值一定能分布在hash环中,我们假设最理想的情况这三个hash值的分布刚好均分这个环
- 假设我们这时候有一条数据是(中国,666)要发request存储进服务器,那么我们是不是可以同理把 中国 求hash值后放进这个环里【不需要求余了】
- 注意这个hash环是有很巨量的数据的,担心3个服务器的hash值会发生hash碰撞【就是重复】纯属是杞人忧天。
- 放到这个环里后,就可以判断该位置顺时针遇到的第一个服务器是谁就存到谁那里,比如这里hash值是20,顺时针第一个遇到的就是m2,直接存到m2中。
- 如何实现?现在对应用场景的步骤进行简单介绍。就一个二分法判断位置就行了。每个机器都有自己负责的段。
- 好处:解决了传统求余的负载均衡的增减服务器代价是全量的问题。现在增减服务器只会影响一个服务器的负责的hash值区域,这数据迁移的代价就不是一个全量问题了呀,这是效率蹭蹭蹭的往上涨。比如加一个m4,就只对原本m3负责的区域进行划分就行。
- 新的问题:hash值在什么时候才是分布均匀的?数据量巨大的时候,但是我们放进去hash环的只是3台机器?这很难做到均分啊。不均分带来什么后果?3台机器负责的段有多有少,而且差值可能很大,那么就会导致不同机器的负载压力区别会很大,所谓的负载均衡从何谈起?
2.2.2 虚拟节点技术
- 解决均匀分配hash值问题
- 我们给m1,m2,m3各分配1000个字符串【字符串可以是随意的,目的是去提供hash值】,这些字符串是能各自对应出1000个hash值的,也就是每台服务器都能在hash环中有1000个虚拟节点,扩大了负责的区域和密度。往这个方向想是不是就能实现均分存储数据的时机了?我们的m1,m2,m3都不再去抢环了,都是虚拟节点去抢。
- 所有虚拟节点都遵循hash环的规则去按照顺时针抢环,所有a节点抢到的都给m1,所有b节点抢到的都给m2,所有c抢到的都给m3。怎么给?给个地址定位路由过去不就行了?
- 因为abc虚拟节点的数量够多,满足hash值在巨量数据下均分原则。然后abc都是分别从属m1,m2,m3的,是不是就相当于m1,m2,m3在均分环?
- abc有n多个节点【大量】是十分乱的去把整个环均分掉,因为样本量变多了!服务器就可以间接满足hash函数的均匀性了。
- 加机器怎么办?直接分配1000个d虚拟节点给m4,d也会均分到环中去抢数据,同样因为d在抢数据,那abc是不是数据就少了?负载是不是就又均衡了?妙的要死,为什么,因为hash函数在巨量下的均匀性。
- 如果m1负载能力特别强,m2的负载能力一般,m3的负载能力弱。那你给m1分配多一点虚拟节点抢多点2000个,m2给1000个,m3承载能力弱就给少一点压力500个,抢少一点。这样就不仅能实现负载均衡,还能实现负载管理,妙的一批!