hash的算法 java_【数据结构与算法】一致性Hash算法及Java实践

追求极致才能突破极限

一、案例背景

1.1 系统简介

首先看一下系统架构,方便解释:

2769817f575ab280bce5b1cbbc5dfbfc.png

页面给用户展示的功能就是,可以查看任何一台机器的某些属性(以下简称系统信息)。

消息流程是,页面发起请求查看指定机器的系统信息到后台,后台可以查询到有哪些server在提供服务,根据负载均衡算法(简单的轮询)指定由哪个server进行查询,并将消息发送到Kafka,然后所有的server消费Kafka的信息,当发现消费的信息要求自己进行查询时,就连接指定的machine进行查询,并将结果返回回去。

Server是集群架构,可能动态增加或减少。

至于架构为什么这么设计,不是重点,只能说这是符合当时环境的最优架构。

1.2 遇到问题

遇到的问题就是慢,特别慢,经过初步核实,最耗时的事是server连接machine的时候,基本都要5s左右,这是不能接受的。

1.3 初步优化

因为耗时最大的是server连接machine的时候,所以决定在server端缓存machine的连接,经过测试如果通过使用的连接缓存进行查询,那么耗时将控制在1秒以内,满足了用户的要求,不过还有一个问题因此产生,那就是根据现有负载均衡算法,假如server1已经缓存了到machine1的连接,但是再次查询时,请求就会发送到下一个server,如server2,这就导致了两个问题,一是,重新建立了连接耗时较长,二是,两个server同时缓存着到machine1的连接,造成了连接浪费。

1.4 继续优化

一开始想到最简单的就是将查询的machine进行hash计算,并除sever的数量取余,这样保证了查询同一个machine时会要求同一个server进行操作,满足了初步的需求。但是因为server端是集群,机器有可能动态的增加或减少,假如根据hash计算,指定的 machine会被指定的server连接,如下图:

b41ad34019fee6ba579d1473cc65f425.png

然后又增加了一个server,那么根据当前的hash算法,server和machine的连接就会变成如下:

54028d7435784260009191988e8b4066.png

可以发现,四个machine和server的连接关系发生变化了,这将导致4次连接的初始化,以及四个连接的浪费,虽然server集群变动的几率很小,但是每变动一次将有一半的连接作废掉,这还是不能接受的,当时想的最理想的结果是:

当新增机器的时候,原有的连接分一部分给新机器,但是除去分出的连接以外保持不变

当减少机器的时候,将减少机器的连接分给剩下的机器,但剩下机器的原有连接不变

简单来说,就是变动不可避免,但是让变动最小化。根据这种思想,就想到了一致性hash,觉得这个应该可以满足要求。

二、使用一致性Hash解决问题

一致性Hash的定义或者介绍在第三节,现在写出一致性Hash的Java的解决方法。只写出示例实现代码,首先最重要的就是Hash算法的选择,根据现有情况以及已有Hash算法的表现,选择了FNV Hash算法,以下是其实现:

public static intFnvHash(String key) {final int p = 16777619;long hash = (int) 2166136261L;for (int i = 0,n = key.length(); i < n; i++){

hash= (hash ^ key.charAt(i)) *p;

}

hash+= hash << 13;

hash^= hash >> 7;

hash+= hash << 3;

hash^= hash >> 17;

hash+= hash << 5;return ((int) hash & 0x7FFFFFFF);

}

然后是对能提供服务的server进行预处理:

public static ConcurrentSkipListMapinit(){//创建排序Map方便后面的计算

ConcurrentSkipListMap servers=new ConcurrentSkipListMap<>();//获得可以提供服务的server

List serverUrls=Arrays.asList("192.168.2.1:8080","192.168.2.2:8080","192.168.2.3:8080");//将server依次添加到Map中

for(String serverUrl:serverUrls){

servers.put(FnvHash(serverUrl), serverUrl);//以下三个是当前server的三个虚拟节点,Hash不同

servers.put(FnvHash(serverUrl+"#1"), serverUrl);

servers.put(FnvHash(serverUrl+"#2"), serverUrl);

servers.put(FnvHash(serverUrl+"#3"), serverUrl);

}returnservers;

}

这段代码将能提供的server放入排序Map,键为其Hash值,值为server的主机和IP,接下来就要对每一个请求的要连接的machin计算需要哪一个server进行连接:

/***@parammachine 要连接的机器

*@paramservers 可提供服务的server

*@return

*/

private static String getServer(int machine, ConcurrentSkipListMapservers) {int left=Integer.MAX_VALUE;int right=Integer.MAX_VALUE;int leftDis=0;int rightDis=0;for(Entryserver:servers.entrySet()){int key=server.getKey();if(key

left=key;

}else{

right=key;

}if(right!=Integer.MAX_VALUE){break;

}

}if(left==Integer.MAX_VALUE){

left=servers.lastKey();

leftDis=Integer.MAX_VALUE-left+machine;

}else{

leftDis=machine-left;

}if(right==Integer.MAX_VALUE){

right=servers.firstKey();

rightDis=Integer.MAX_VALUE-machine+right;

}else{

rightDis=right-machine;

}return servers.get(rightDis<=leftDis?right:left);

}

这个方法就是计算,具体逻辑可以在看完下一节有更深的了解。

经过上面的三个方法就解决了上面提出的要求,经过测试也完美,或许算法还看不懂,也或许一致Hash算法还不知道是什么,虚拟节点是什么,但是现在应该了解需求是怎么产生的,已经通过什么满足了要求,现在唯一要做的就是了解一致性Hash了,下面进行介绍。

三、一致性Hash介绍

3.1 理论简介

一致性Hash的简介,摘自百度百科。

一致性哈希算法在1997年由麻省理工学院提出,设计目标是为了解决因特网中的热点(Hot spot)问题。一致性哈希提出了在动态变化的Cache环境中,哈希算法应该满足的4个适应条件:

均衡性(Balance):

平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。

单调性(Monotonicity):

单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲区加入到系统中,那么哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲区中去,而不会被映射到旧的缓冲集合中的其他缓冲区。(这段翻译信息有负面价值的,当缓冲区大小变化时一致性哈希(Consistent hashing)尽量保护已分配的内容不会被重新映射到新缓冲区。)

分散性(Spread):

在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。

负载(Load):

负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

3.2 设计实现

一般的一致性Hash的设计实现都是按照如下方式:

首先所有的Hash值应该构成一个环,就像钟表的时刻一样,也就是说有明确的Hash最大值,环内Hash的数量一般为2的32次方:

cc7ae1741d6a970f19cc9028f4148c93.png

将server通过Hash计算映射到环上,注意选取能区别开server的唯一属性,比如ip加端口:

a9ab19e6b9ef6a257a09efbcac2f2164.png

然后所有的把所有的请求使用唯一的属性计算Hash值,然后请求到最近的server上面:

dba6f25e50f3b7a30f29b44863e9079c.png

ae2ce7abc342a66fc7b67675d6dcf7bd.png

假如有新机器加入时:

d052f9c80031f81d5a2648cd19d748f7.png

新机器相邻的请求会被重新定向到新的server,如果有机器挂掉的话,挂掉机器的请求也会重新分配给就近的server:

a1ed587192c84a6ab8dde42c66c99c7b.png

通过上面的图例讲解,应该可以看出环形设计的好处,那就是不管新增还是减少机器,变动的都是变动机器附近的请求,已有请求的映射不会变动到已有的节点上。

四、对一致性Hash的理解

4.1 应用场景

通过一致性Hash的特性来看,一致性Hash极力保证变动的最小化,比较适用于有状态连接,如果连接是无状态的,那么完全没必要使用这种算法,轮询或者随机都是可以的。效率要比一致性Hash高,省去了每一次请求的计算过程。

4.2 环的Hash数量的选择

本质上没有特殊的要求,选取的原则可以考虑以下几点:

Hash数量最好最够多,因为要考虑未来新增server的情况,以及虚拟节点的添加

Hash数量的最大值在int范围内即可,int最大值已经足够大,大于int的会相对增加计算和存储成本

Hash数量的最大值的另一个参考要点,就是选取Hash算法的最大值

所以上面的例子,环Hash数量选择了2^32,恰好fnv Hash算法的最大值也是它,FNV Hash算法参照此。

4.3 虚拟节点的作用

看过上面代码的应该知道,对server进行Hash的时候,会同时创建server的几个虚拟节点,它们同样代表着它们的server,有如下作用:

防止server的Hash重复,虽然Hash重复的概率少之又少,但是依然不能完全避免,所以通过使用多个虚拟节点,可以避免因server的Hash重复导致server被完全覆盖掉

有利于负载均衡,如果每个server只有一个节点,那么有可能分布的不均匀,这时候通过多个虚拟节点,可以增加均匀分布的可能性,当然这依赖于Hash算法的选择

至于虚拟节点的数量,这个没有硬性要求,节点的数量越多,负载均衡越好,但是计算量也越大,如果考虑到server集群的易变性,每一次请求都需要重新计算server及其虚拟节点的Hash值,那么节点的数量不要太大,不然也是一个性能的瓶颈。

4.4 Hash算法的选择

Hash算法有很多种,上面fnv hash的可以参考一下,至于其他的,考虑以下几点就可以:

不要自己写Hash算法,用已有的就可以,出于学习的目的可以写,生产环境用已有的Hash算法

算法速度一定要快

同一个输入的值,要有相同的输出

Hash值足够散列,Hash碰撞概率低

考虑以上几点就可以了,后续会针对Hash算法,写一篇博客。

4.5 一致性Hash的替代

不用一致Hash可不可以,能不能满足相同的需求,答案是可以的,那就是主动维护一个路由表。基本要做以下操作:

首先获得当前提供服务的server

当有请求来临时,先判断当前请求是否已有对应的server,若有交由对应的server,若无,选择负载最低的一个server,并存记录

当server挂掉以后,新的请求重新走2步骤

当有新的server加入时,可以主动负载均衡,也可以重新走2步骤

优缺点简单说一下:

优点:

负载更加均衡,甚至可以保证完全的均衡,因为不依赖Hash的不确定性

整个分配过程人为掌握,当某些请求必须分配到指定的server上,修改更简单

缺点:

编码量大,需要严格测试

需要主动维护一个路由表,存储是一个需要考虑的问题

请求量大时,路由表容量会增大,可以考虑存入Redis中

以上就是我对一致Hash的理解,以及我在项目中的应用,希望可以帮助到有需要的人。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/367735.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java EE CDI依赖关系消歧示例

在本教程中&#xff0c;我们将向您展示如何避免CDI bean中的依赖关系消除歧义。 在CDI中&#xff0c;我们可以为应用程序中不同客户端的接口的多个实现实现依赖项注入。 依赖关系消除歧义的问题是客户端如何在不同的实现中调用特定的实现&#xff0c;而不会发生任何错误。 为了…

linux java 安装配置_类Linux环境安装jdk1.8及环境变量配置详解

配置很简单&#xff0c;但是每次都要查一下&#xff0c;索性就记录下1. 安装前准备1.1 创建安装目录&#xff0c;习惯上通常安装在/usr/local/jdk8目录下mkdir /usr/local/jdk81.2 查看是否安装过jdk&#xff0c;安装前要把之前的删除干净# 通过jdk环境变量查看echo $JAVA_HOME…

Mac版Anaconda安装R语言iGraph包

Anaconda官网给出的R语言Igraph包安装方法&#xff1a;https://anaconda.org/r/r-igraph 查阅Anaconda官方文档&#xff0c;可以通过以下控制台命令安装R语言Igraph包。 conda install -c r r-igraph 在控制台执行后&#xff0c;系统可能会提示未找到conda指令&#xff0c; 所以…

Html5中Canvas(画布)的使用

什么是 Canvas&#xff1f;HTML5 的 canvas 元素使用 JavaScript 在网页上绘制图像。画布是一个矩形区域&#xff0c;您可以控制其每一像素。canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。创建 Canvas 元素向 HTML5 页面添加 canvas 元素。规定元素的 id、宽…

多个动态包含一个JSF标签

每个JSF开发人员都知道ui&#xff1a;include和ui&#xff1a;param标签。 您可以包括一个facelet&#xff08;XHTML文件&#xff09;并传递一个对象&#xff0c;该对象将在包含的facelet中可用&#xff0c;如下所示&#xff1a; <ui:include src"/sections/columns.x…

[JLOI2014]松鼠的新家

嘟嘟嘟 这还是一道树链剖分板子题呀&#xff01; 从1到n - 1枚举a[i]&#xff0c;每一次使节点a[i]到a[i 1]的路径加1&#xff0c;但这样的话除a[1]&#xff0c;每一个点都多加了一个1&#xff0c;所以输出答案的时候减1即可。 1 #include<cstdio>2 #include<iostrea…

CSS3-背景(background-image、background-size、background-origin、background-clip)

CSS3中新的背景属性&#xff1a;background-image、background-size、background-origin、background-clip 背景图片&#xff1a;background-image CSS3中可以通过background-image属性添加背景图片。 不同的背景图像和图像用逗号隔开&#xff0c;所有的图片中显示在最顶端的为…

使用String.intern()减少内存使用

时不时地会有一个垂死的生产应用程序。 而且您知道您需要尽快对其进行修补。 我们也一样&#xff0c;并认为分享最近的一个战争故事将很有趣。 在这种情况下&#xff0c;我们就有机会使用String.intern&#xff08;&#xff09;之类的简单补丁来修补应用程序。 但是&#xff0c…

CSS实现比翼双飞和圣杯布局模型效果

圣杯模型和比翼双飞模型主要的特指1.首先加载的是中间部分&#xff0c;其次是左边&#xff0c;然后是右边 2.中间是自适应&#xff0c;二边是定宽 实现方法1.grid2.margin float position:releative父亲加padding 挤进去3.margin float box-size:border-box 1.gridhtml:&l…

jmeter创建高级测试计划

如果应用程序使用重写地址而不是使用cookie存储信息&#xff0c;需要做一些额外的工作去测试程序 为了正确的响应重写地址&#xff0c;jmeter 需要解析 从服务器获取html 并且检索会话ID, 1 合理利用pre-processors-http url rewriting modifier 来完成&#xff0c;简单的输入s…

单元测试技巧:创建描述性测试

您的单元测试应尽可能具有描述性。 他们给您的反馈应该非常清楚&#xff0c;您甚至不需要启动调试器并一步一步地检查代码来检查局部变量。 为什么&#xff1f; 因为那需要时间&#xff0c;而且我们很懒&#xff0c;对吗&#xff1f; 为此&#xff0c;您需要创建描述性测试。 有…

vue.js--基础事件定义,获取数据,执行方法传值

<template><div id"app"> <h1>{{ msg }}</h1> <br> <button v-on:click"run1()"> 第一种写法</button> <br> <button clickrun2()> 第二种写法</button> <br> <button clickgetMsg(…

Spring集成–强大的拆分器聚合器

坚固是什么意思&#xff1f; 在本文的上下文中&#xff0c;健壮性是指在不立即返回到调用者的情况下管理流中的异常条件的能力。 在某些处理方案中&#xff0c; n个 m个回答足以做出结论。 通常具有这些趋势的示例处理场景是&#xff1a; 财务&#xff0c;保险和预订系统的…

填充一个池需要多少个线程?

近几个月来&#xff0c;我们一直看到一小部分但持续的操作失败&#xff0c;并带有一个奇怪的异常– org.springframework.jdbc.CannotGetJdbcConnectionException –“无法获得JDBC连接&#xff1b; 嵌套异常是java.sql.SQLException&#xff1a;客户端尝试检出Connection的尝试…

@font-face 用字体画图标

HTML 1 <body>2 <!-- ul.layout>li*5>a[href#]>i.icon -->3 <!-- Sublime Text 快捷拼写 -->4 <ul class"layout">5 <li><a href"#"><i class"icon">&#xe601;</…

java mapfile_基于文件的数据结构:关于MapFile

MapFile是已经排过序的SequenceFile&#xff0c;它有索引&#xff0c;所以可以按键查找1.MapFile的写操作MapFile的写操作类似于SequenceFile的写操作。新建一个MapFile.Writer实例&#xff0c;然后调用append()方法顺序写入文件内容。如果不按顺序写入&#xff0c;就抛出一个I…

java 循环依赖_解决Java循环依赖的问题

最近看到一个问题&#xff1a;如果有两个类A和B&#xff0c;A类中有一个B的对象b&#xff0c;B类中有一个A的对象a&#xff0c;那么如何解决这两个类的相互依赖关系呢&#xff0c;几天就给大家说一下Java的循环依赖&#xff0c;raksmart服务器。举个栗子1&#xff1a;可以看到A…

Intellij IDEA 将工程转换成maven工程 详解

1> 右键工程&#xff0c;点击 Add Framework Support2> 选中 Maven&#xff0c;再点击 OK3> 工程根目录自动生成 pom.xml 文件&#xff0c;这样 工程就支持 Maven版权声明&#xff1a;本文为博主原创文章&#xff0c;未经博主允许不得转载。 http://blog.csdn.net/che…

数据结构(七)排序---直接插入排序(稳定)

经典排序算法----直接插入排序算法及其改进&#xff08;稳定&#xff09; 定义&#xff1a; 直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中&#xff0c;从而得到一个新的&#xff0c;记录数加一的有序表。 实现思想 我们预留了一个哨兵&#xff0c;这里我们将…

java嵌套类型 无法隐藏外层类型_java内部类深入详解 内部类的分类 特点 定义方式 使用...

java内部类 内部类的分类 特点 定义方式 使用 外部类调用内部类 多层嵌套内部类 内部类访问外部类属性 接口中的内部类 内部类的继承 内部类的覆盖 局部内部类 成员内部类 静态内部类 匿名内部类内部类定义将一个类定义放到另一个类的内部,这就是内部类内部类与组合是完…