随机算法的实现也非常简单,也就是当客户端哀求到来时,每次都会从已设置的服务器列表中随机抽取一个节点处理惩罚。
实现如下:
// 随机战略类:随机抽取集群中的一个节点处理惩罚哀求public class Random {// 随机数产生器,用于产生随机因子static java.util.Random random = new java.util.Random();public static String getServer(){// 从已设置的服务器列表中,随机抽取一个节点处理惩罚哀求return Servers.SERVERS.get(random.nextInt(Servers.SERVERS.size()));}}上述该算法的实现,非常明白,通过 java.util 包中自带的 Random 随机数产生器,从服务器列表中随机抽取一个节点处理惩罚哀求,该算法的结果也不测试了,各人估计一眼就能看明白。
随机算法的上风:个人看来该算法单独利用的意义并不大,一样平常会共同下面要讲的权重战略协同利用。
随机算法的劣势:
无法公道地将哀求均派到每台服务器上。
由于处理惩罚哀求的目的服务器不明白,因此也无法满足须要记载状态的哀求。
可以大概在肯定程度上发挥出高设置的呆板性能,但充满不确定因素。
| 权重算法
权重算法是创建在其他基础算法之上推出的一种概念,权重算法并不能单独设置,由于权重算法无法做到哀求分发的调理,以是一样平常权重会共同其他基础算法联合利用。
如:轮询权重算法、随机权重算法等,如答应以让之前的两种基础调理算法更为“人性化”一些。
权重算法是指对于集群中的每个节点分配一个权重值,权重值越高,该节点被分发的哀求数也会越多,反之同理。
如许做的利益非常显着,也就是可以大概充实思量呆板的硬件设置,从而分配差别权重值,做到“能者多劳”。
那怎样实现呢,先来看看随机权重的实现:
public class Servers{// 在之前是Servers类中再参加一个权重服务列表public static Map<String, Integer> WEIGHT_SERVERS = new LinkedHashMap<>();static {// 设置集群的所有节点信息及权重值WEIGHT_SERVERS.put("44.120.110.001:8080",17);WEIGHT_SERVERS.put("44.120.110.002:8081",11);WEIGHT_SERVERS.put("44.120.110.003:8082",30);}}// 随机权重算法public class Randomweight {// 初始化随机数生产器static java.util.Random random = new java.util.Random();public static String getServer(){// 盘算总权重值int weightTotal = 0;for (Integer weight : Servers.WEIGHT_SERVERS.values()) {weightTotal += weight;}// 从总权重的范围内随机天生一个索引int index = random.nextInt(weightTotal);System.out.println(index);// 遍历整个权重集群的节点列表,选择节点处理惩罚哀求String targetServer = "";for (String server : Servers.WEIGHT_SERVERS.keySet()) {// 获取每个节点的权重值Integer weight = Servers.WEIGHT_SERVERS.get(server);// 假如权重值大于产生的随机数,则代表此次随机分配应该落入该节点if (weight > index){// 直接返回对应的节点行止理惩罚本次哀求并制止循环targetServer = server;break;}// 假如当前节点的权重值小于随机索引,则用随机索引减去当前节点的权重值,// 继承循环权重列表,与其他的权重值举行对比,// 终极该哀求总会落入到某个IP的权重值范围内index = index - weight;}// 返回选中的目的节点return targetServer;}public static void main(String[] args){// 利用for循环模拟10个客户端哀求测试for (int i = 1; i <= 10; i++){System.out.println("第"+ i + "个哀求:" + getServer());}}}/********运行结果********/第1个哀求:44.120.110.003:8082第2个哀求:44.120.110.001:8080第3个哀求:44.120.110.003:8082第4个哀求:44.120.110.003:8082第5个哀求:44.120.110.003:8082第6个哀求:44.120.110.003:8082第7个哀求:44.120.110.003:8082第8个哀求:44.120.110.001:8080第9个哀求:44.120.110.001:8080第10个哀求:44.120.110.002:8081上面这个算法对比之前的根本实现,大概略微有些复杂难明,我们先上个图:
细致观看上图后,逻辑应该会清楚许多,大要捋一下思绪:
实现同等性哈希算法的核心结构在于哈希环,前面讲到过同等性哈希是基于 2^32 做取模。
那么起首可以将二的三十二次方想象成一个圆,这个圆统共由 2^32 个点构成,如下:
圆环的正上方第一个点代表 0,0 右侧的点按照 1、2、3、4....的次序依此类推,直到 2^32-1,也就是说 0 左侧的第一个点代表着 2^32-1。
终极这个在逻辑上由 2^32 个点构成的圆,被称为哈希环。
联合之前的缓存案例,假设有四台缓存服务器 A、B、C、D,然后再通过每台服务器的 IP 哈希值取模 2^32,终极一定会得到一个 2^32 范围之内的整数,这个数在哈希环上定然也对应着一个点。
那么每台服务器的 IP 就可以映射到哈希环上,如下:
到此时,服务器已经和哈希环创建起了接洽,那么此时当客户端发送哀求时,又可以通过雷同的盘算方式,将客户端须要利用的缓存 Key 举行雷同的哈希取模,然后同样将其映射到哈希环上。
比方写入一条缓存 name=竹子,如下:
那么此时该缓存纠结要落入到哪台服务器呢?答案是 B,为什么?由于在哈希环结构中,沿着顺时针方向走,遇到的第一台服务器是 B,以是终极会落到 B 服务器上。
固然,假如同等性哈希算法被用于哀求分发,那么就以用户的 IP 作为哈希取模的条件,如许就能确保同一个客户端的所有哀求都会被分发到同一台服务器。
同等性哈希算法中,就利用哈希环结构+哈希取模判断每个哀求该落入的服务器,由于服务器 IP、客户端 IP 或缓存的 Key 都是雷同的,以是在服务器数量稳定的情况,雷同的哈希条件举行哈希取模,终极盘算出来的值永久都是雷同的。
然后再通过盘算出的值,在哈希环结构上举行顺时针查找,可以大概定位到的服务器也是雷同的,以是雷同属性的哀求永久会落入到同一服务器。
| 哈希环的映射偏移标题
经过上述分析后,似乎发现同等性哈希算法没啥大毛病,但上述中属于“理想状态”:
可偏偏理想很丰满,现实却很骨感,现实映射服务器 IP 的过程中,大概会出现如下情况:
由于服务器 IP 哈希取模后,无法确保哈希得到的数字可以大概匀称分布,因此就有大概造成如上情况,所有的服务器IP都被映射在“一块儿”,终极导致 A 服务器承载了 90% 以上的访问压力。
| 映射偏移造成的宕机连锁反应
接上述,假如服务器 IP 映射在哈希环上出现偏移,在大流量的冲击下,这种情况很轻易导致整个集群崩塌,起首是A扛不住并发冲击,宕机下线,紧接着流量交给 B,B 也扛不住,接着宕机,然后 C.....
因此哈希环映射偏移标题大概会造成的一系列连锁反应,以是在同等性哈希算法中,为了确保整个集群的坚固性,提出了一种捏造节点的概念来办理此标题。
捏造节点实在本质上就是真实服务器节点的复制品,捏造节点映射的 IP 都是指向于真实服务器的。
就类似平常 .EXE 软件的快捷方式,现在为 QQ 创建了一个快捷方式,然后拷贝到了十个差别的目次下,但本质上这十个快捷方式指向的启动文件都是雷同 exe 步调。
哈希环中的捏造节点也同理,如下:
从上图中可以看出,A、B、C、D 四台服务器分别都映射出了一个捏造节点,引入捏造节点后会显着感觉出来,本来 A 服务器须要承载 90% 以上的流量,但此刻映射出的捏造节点大大减轻了 A 的压力,将流量均派到了集群中的每个节点。
在同等性哈希算法的现实应用场景中,绝非只映射一个捏造节点,每每会为一个真实节点映射数十个捏造节点,以便于减小哈希环偏移所带来的影响。
同时,捏造节点的数量越多,哀求在分发时也能更匀称的分布,哈希环终极结构如下:
| Java 实现同等性哈希算法
讲了这么多,那么同等性哈希算法究竟怎样实现呢?接下来一起看看:
public class Servers {public static List<String> SERVERS = Arrays.asList("44.120.110.001:8080","44.120.110.002:8081","44.120.110.003:8082","44.120.110.004:8083","44.120.110.005:8084");}public class ConsistentHash {// 利用有序的红黑树结构,用于实现哈希环结构private static TreeMap<Integer,String> virtualNodes = new TreeMap<>();// 每个真实节点的捏造节点数量private static final int VIRTUAL_NODES = 160;static {// 对每个真实节点添加捏造节点,捏造节点会根据哈希算法举行散列for (String serverIP : Servers.SERVERS) {// 将真实节点的IP映射到哈希环上virtualNodes.put(getHashCode(serverIP), serverIP);// 根据设定的捏造节点数量举行捏造节点映射for (int i = 0; i < VIRTUAL_NODES; i++){// 盘算出一个捏造节点的哈希值(只要差别即可)int hash = getHashCode(serverIP + i);// 将捏造节点添加到哈希环结构上virtualNodes.put(hash, serverIP);}}}public static String getServer(String IP){int hashCode = getHashCode(IP);// 得到大于该Hash值的子红黑树SortedMap<Integer, String> sortedMap = virtualNodes.tailMap(hashCode);// 得到该树的第一个元素,也就是最小的元素Integer treeNodeKey = sortedMap.firstKey();// 假如没有大于该元素的子树了,则取整棵树的第一个元素,相称于取哈希环中的最小元素if (sortedMap == null)treeNodeKey = virtualNodes.firstKey();// 返回对应的捏造节点名称return virtualNodes.get(treeNodeKey);}// 哈希方法:用于盘算一个IP的哈希值public static int getHashCode(String IP){final int p = 1904390101;int hash = (int)1901102097L;for (int i = 0; i < IP.length(); i++)hash = (hash ^ IP.charAt(i)) * p;hash += hash << 13;hash ^= hash >> 7;hash += hash << 3;hash ^= hash >> 17;hash += hash << 5;// 假如算出来的值为负数则取其绝对值if (hash < 0)hash = Math.abs(hash);return hash;}public static void main(String[] args){// 用for循环模拟五个差别的IP访问for (int i = 1; i <= 5; i++){System.out.println("第"+ i + "个哀求:" + getServer("192.168.12.13"+i));}System.out.println("-----------------------------");// 用for循环模拟三个雷同的IP访问for (int i = 1; i <= 3; i++){System.out.println("第"+ i + "个哀求:" + getServer("192.168.12.131"));}}}/********输出结果*******/第1个哀求:44.120.110.002:8081第2个哀求:44.120.110.003:8082第3个哀求:44.120.110.004:8083第4个哀求:44.120.110.003:8082第5个哀求:44.120.110.004:8083-----------------------------第1个哀求:44.120.110.002:8081第2个哀求:44.120.110.002:8081第3个哀求:44.120.110.002:8081上述便是 Java 实现同等性哈希算法的全过程,实在并不难明白,内里用到了 TreeMap 实现了哈希环结构,而且指定了每个服务器节点的捏造节点数量,同时实现了一个简单的哈希方法,用于盘算入参的哈希值。
算法过程如下: