一致性哈希算法学习及JAVA代码实现分析

简介:

1,对于待存储的海量数据,如何将它们分配到各个机器中去?---数据分片与路由

当数据量很大时,通过改善单机硬件资源的纵向扩充方式来存储数据变得越来越不适用,而通过增加机器数目来获得水平横向扩展的方式则越来越流行。因此,就有个问题,如何将这些海量的数据分配到各个机器中?数据分布到各个机器存储之后,又如何进行查找?这里主要记录一致性Hash算法如何将数据分配到各个机器中去。

 

2,衡量一致性哈希算法好处的四个标准:

①平衡性:平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。②单调性:单调性是指如果已经有一些数据通过哈希分配到了相应的机器上,又有新的机器加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的机器中去,而不会被映射到旧的机器集合中的其他机器上。

这里再解释一下:就是原有的数据要么还是呆在它所在的机器上不动,要么被迁移到新的机器上,而不会迁移到旧的其他机器上。

③分散性④负载参考这里

 

3,一致性哈希的原理:

由于一般的哈希函数返回一个int(32bit)型的hashCode。因此,可以将该哈希函数能够返回的hashCode表示成一个范围为0---(2^32)-1 环。

将机器的标识(如:IP地址)作为哈希函数的Key映射到环上。如:

hash(Node1) =Key1      hash(Node2) = Key2,借用一张图如下:

 

 

 同样,数据也通过相同的哈希函数映射到环上。这样,按照顺时针方向,数据存放在它所在的顺时针方向上的那个机器上。这就是一致性哈希算法分配数据的方式!

 

4,JAVA实现一致性哈希算法的代码分析:

设计哈希函数

这里采用了MD5算法,主要是用来保证平衡性,即能够将机器均衡地映射到环上。貌似用Jdk中String类的hashCode并不能很好的保证平衡性。

复制代码
 1 import java.security.MessageDigest;
 2 import java.security.NoSuchAlgorithmException;
 3 
 4 /*
 5  * 实现一致性哈希算法中使用的哈希函数,使用MD5算法来保证一致性哈希的平衡性
 6  */
 7 public class HashFunction {
 8     private MessageDigest md5 = null;
 9 
10     public long hash(String key) {
11         if (md5 == null) {
12             try {
13                 md5 = MessageDigest.getInstance("MD5");
14             } catch (NoSuchAlgorithmException e) {
15                 throw new IllegalStateException("no md5 algrithm found");
16             }
17         }
18 
19         md5.reset();
20         md5.update(key.getBytes());
21         byte[] bKey = md5.digest();
22         //具体的哈希函数实现细节--每个字节 & 0xFF 再移位
23         long result = ((long) (bKey[3] & 0xFF) << 24)
24                 | ((long) (bKey[2] & 0xFF) << 16
25                         | ((long) (bKey[1] & 0xFF) << 8) | (long) (bKey[0] & 0xFF));
26         return result & 0xffffffffL;
27     }
28 }
复制代码

 

实现一致性哈希算法:

为什么要引入虚拟机器节点?它的作用是什么?

引入虚拟机器节点,其目的就是为了解决数据分配不均衡的问题。因为,在将实际的物理机器映射到环上时,有可能大部分机器都映射到环上的某一个部分(比如左半圆上),而通过引入虚拟机器节点,在进行机器hash映射时,不是映射具体机器,而是映射虚拟机器,并保证虚拟机器对应的物理机器是均衡的---每台实际的机器对应着相等数目的Virtual nodes。如下图:

 

 

如何解决集群中添加或者删除机器上需要迁移大量数据的问题?

假设采用传统的哈希取模法,设有K台物理机,H(key)=hash(key) mod K 即可实现数据分片。但当K变化时(如新增一台机器),所有已经映射好的数据都需要重新再映射。H(key)=hash(key) mod (K+1)。

一致性哈希采用的做法如下:引入一个环的概念,如上面的第一个图。先将机器映射到这个环上,再将数据也通过相同的哈希函数映射到这个环上,数据存储在它顺时针走向的那台机器上。以环为中介,实现了数据与机器数目之间的解藕。这样,当机器的数目变化时,只会影响到增加或删除的那台机器所在的环的邻接机器的数据存储,而其他机器上的数据不受影响。

 

在具体JAVA实现代码中,定义了一个TreeMap<k, V>用来保存虚拟机器节点到实际的物理机器的映射。机器以字符串形式来标识,故hash函数的参数为String

复制代码
1 for (int i = 0; i < numberOfReplicas; i++)
2             // 对于一个实际机器节点 node, 对应 numberOfReplicas 个虚拟节点
3             /*
4              * 不同的虚拟节点(i不同)有不同的hash值,但都对应同一个实际机器node
5              * 虚拟node一般是均衡分布在环上的,数据存储在顺时针方向的虚拟node上
6              */
7             circle.put(hashFunction.hash(node.toString() + i), node);
复制代码

 

而对于 数据的存储而言,逻辑上是按顺时针方向存储在虚拟机器节点中,虚拟机器节点通过TreeMap知道它实际需要将数据存储在哪台物理机器上。此外,TreeMap中的Key是有序的,而环也是顺时针有序的,这样才能当数据被映射到两台虚拟机器之间的弧上时,通过TreeMap的 tailMap()来寻找顺时针方向上的下一台虚拟机。

1     if (!circle.containsKey(hash)) {//数据映射在两台虚拟机器所在环之间,就需要按顺时针方向寻找机器
2             SortedMap<Long, T> tailMap = circle.tailMap(hash);
3             hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
4         }

 

 完整代码:

复制代码
 1 import java.util.Collection;
 2 import java.util.HashSet;
 3 import java.util.Iterator;
 4 import java.util.Set;
 5 import java.util.SortedMap;
 6 import java.util.SortedSet;
 7 import java.util.TreeMap;
 8 import java.util.TreeSet;
 9 
10 public class ConsistentHash<T> {
11     private final HashFunction hashFunction;
12     private final int numberOfReplicas;// 节点的复制因子,实际节点个数 * numberOfReplicas =
13                                         // 虚拟节点个数
14     private final SortedMap<Long, T> circle = new TreeMap<Long, T>();// 存储虚拟节点的hash值到真实节点的映射
15 
16     public ConsistentHash(HashFunction hashFunction, int numberOfReplicas,
17             Collection<T> nodes) {
18         this.hashFunction = hashFunction;
19         this.numberOfReplicas = numberOfReplicas;
20         for (T node : nodes)
21             add(node);
22     }
23 
24     public void add(T node) {
25         for (int i = 0; i < numberOfReplicas; i++)
26             // 对于一个实际机器节点 node, 对应 numberOfReplicas 个虚拟节点
27             /*
28              * 不同的虚拟节点(i不同)有不同的hash值,但都对应同一个实际机器node
29              * 虚拟node一般是均衡分布在环上的,数据存储在顺时针方向的虚拟node上
30              */
31             circle.put(hashFunction.hash(node.toString() + i), node);
32     }
33 
34     public void remove(T node) {
35         for (int i = 0; i < numberOfReplicas; i++)
36             circle.remove(hashFunction.hash(node.toString() + i));
37     }
38 
39     /*
40      * 获得一个最近的顺时针节点,根据给定的key 取Hash
41      * 然后再取得顺时针方向上最近的一个虚拟节点对应的实际节点
42      * 再从实际节点中取得 数据
43      */
44     public T get(Object key) {
45         if (circle.isEmpty())
46             return null;
47         long hash = hashFunction.hash((String) key);// node 用String来表示,获得node在哈希环中的hashCode
48         if (!circle.containsKey(hash)) {//数据映射在两台虚拟机器所在环之间,就需要按顺时针方向寻找机器
49             SortedMap<Long, T> tailMap = circle.tailMap(hash);
50             hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
51         }
52         return circle.get(hash);
53     }
54 
55     public long getSize() {
56         return circle.size();
57     }
58     
59     /*
60      * 查看MD5算法生成的hashCode值---表示整个哈希环中各个虚拟节点位置
61      */
62     public void testBalance(){
63         Set<Long> sets  = circle.keySet();//获得TreeMap中所有的Key
64         SortedSet<Long> sortedSets= new TreeSet<Long>(sets);//将获得的Key集合排序
65         for(Long hashCode : sortedSets){
66             System.out.println(hashCode);
67         }
68         
69         System.out.println("----each location 's distance are follows: ----");
70         /*
71          * 查看用MD5算法生成的long hashCode 相邻两个hashCode的差值
72          */
73         Iterator<Long> it = sortedSets.iterator();
74         Iterator<Long> it2 = sortedSets.iterator();
75         if(it2.hasNext())
76             it2.next();
77         long keyPre, keyAfter;
78         while(it.hasNext() && it2.hasNext()){
79             keyPre = it.next();
80             keyAfter = it2.next();
81             System.out.println(keyAfter - keyPre);
82         }
83     }
84     
85     public static void main(String[] args) {
86         Set<String> nodes = new HashSet<String>();
87         nodes.add("A");
88         nodes.add("B");
89         nodes.add("C");
90         
91         ConsistentHash<String> consistentHash = new ConsistentHash<String>(new HashFunction(), 2, nodes);
92         consistentHash.add("D");
93         
94         System.out.println("hash circle size: " + consistentHash.getSize());
95         System.out.println("location of each node are follows: ");
96         consistentHash.testBalance();
97     }
98     
99 }
复制代码

 

 

参考资料:五分钟理解一致性哈希算法(consistent hashing)

 一致性hash算法 - consistent hashing

一致性哈希算法介绍,及java实现

本文转自hapjin博客园博客,原文链接:http://www.cnblogs.com/hapjin/,如需转载请自行联系原作者


 

相关文章
|
15天前
|
缓存 JavaScript Java
常见java OOM异常分析排查思路分析
Java虚拟机(JVM)遇到内存不足时会抛出OutOfMemoryError(OOM)异常。常见OOM情况包括:1) **Java堆空间不足**:大量对象未被及时回收或内存泄漏;2) **线程栈空间不足**:递归过深或大量线程创建;3) **方法区溢出**:类信息过多,如CGLib代理类生成过多;4) **本机内存不足**:JNI调用消耗大量内存;5) **GC造成的内存不足**:频繁GC但效果不佳。解决方法包括调整JVM参数(如-Xmx、-Xss)、优化代码及使用高效垃圾回收器。
86 15
常见java OOM异常分析排查思路分析
|
8天前
|
设计模式 Java
Java设计模式:组合模式的介绍及代码演示
组合模式是一种结构型设计模式,用于将多个对象组织成树形结构,并统一处理所有对象。例如,统计公司总人数时,可先统计各部门人数再求和。该模式包括一个通用接口、表示节点的类及其实现类。通过树形结构和节点的通用方法,组合模式使程序更易扩展和维护。
Java设计模式:组合模式的介绍及代码演示
|
3天前
|
Java
JAVA并发编程系列(9)CyclicBarrier循环屏障原理分析
本文介绍了拼多多面试中的模拟拼团问题,通过使用 `CyclicBarrier` 实现了多人拼团成功后提交订单并支付的功能。与之前的 `CountDownLatch` 方法不同,`CyclicBarrier` 能够确保所有线程到达屏障点后继续执行,并且屏障可重复使用。文章详细解析了 `CyclicBarrier` 的核心原理及使用方法,并通过代码示例展示了其工作流程。最后,文章还提供了 `CyclicBarrier` 的源码分析,帮助读者深入理解其实现机制。
|
8天前
|
Java 程序员 API
Java中的Lambda表达式:简化代码的秘密武器
在Java 8中引入的Lambda表达式是一种强大的编程工具,它可以显著简化代码,提高可读性。本文将介绍Lambda表达式的基本概念、优势以及在实际开发中的应用。通过具体示例,您将了解如何使用Lambda表达式来简化集合操作、线程编程和函数式编程。让我们一起探索这一革命性的特性,看看它是如何改变Java编程方式的。
20 4
|
8天前
|
Java 开发者
探索Java中的Lambda表达式:简化你的代码
【8月更文挑战第49天】在Java 8的发布中,Lambda表达式无疑是最令人兴奋的新特性之一。它不仅为Java开发者提供了一种更加简洁、灵活的编程方式,而且还极大地提高了代码的可读性和开发效率。本文将通过实际代码示例,展示如何利用Lambda表达式优化和重构Java代码,让你的编程之旅更加轻松愉快。
|
13天前
|
SQL JavaScript 前端开发
基于Java访问Hive的JUnit5测试代码实现
根据《用Java、Python来开发Hive应用》一文,建立了使用Java、来开发Hive应用的方法,产生的代码如下
39 6
|
11天前
|
Java 开发者
探索Java中的Lambda表达式:简化代码,提升效率
【9月更文挑战第14天】本文旨在揭示Java 8中引入的Lambda表达式如何革新了我们编写和管理代码的方式。通过简洁明了的语言和直观的代码示例,我们将一起走进Lambda表达式的世界,了解其基本概念、语法结构以及在实际编程中的应用。文章不仅会展示Lambda表达式的魅力所在,还会指导读者如何在日常工作中有效利用这一特性,以提高编码效率和程序可读性。
|
17天前
|
并行计算 Java 开发者
探索Java中的Lambda表达式:简化代码,提升效率
Lambda表达式在Java 8中引入,旨在简化集合操作和并行计算。本文将通过浅显易懂的语言,带你了解Lambda表达式的基本概念、语法结构,并通过实例展示如何在Java项目中应用Lambda表达式来优化代码,提高开发效率。我们将一起探讨这一现代编程工具如何改变我们的Java编码方式,并思考它对程序设计哲学的影响。
|
存储 缓存 负载均衡
Java实现一致性哈希算法,并搭建环境测试其负载均衡特性(二)
实现负载均衡是后端领域一个重要的话题,一致性哈希算法是实现服务器负载均衡的方法之一,你很可能已在一些远程服务框架中使用过它。下面我们尝试一下自己实现一致性哈希算法。
Java实现一致性哈希算法,并搭建环境测试其负载均衡特性(二)
|
存储 负载均衡 算法
Java实现一致性哈希算法,并搭建环境测试其负载均衡特性(一)
实现负载均衡是后端领域一个重要的话题,一致性哈希算法是实现服务器负载均衡的方法之一,你很可能已在一些远程服务框架中使用过它。下面我们尝试一下自己实现一致性哈希算法。
Java实现一致性哈希算法,并搭建环境测试其负载均衡特性(一)