首次接触一致性哈希是在学习memcached的时候,为了解决分布式服务器的负载均衡或者说选路的问题,一致性哈希算法不仅能够使memcached服务器被选中的概率(数据分布)更加均匀,而且使得服务器的增加和减少对整个分布式存储的影响也较小,也就是说不会引起大范围的数据迁移。

  关于一致性哈希算法的原理和应用我就不多说了,网上一抓一大把,可以看这里这里、或者这里等等。直接上代码:

 /**
* 在这个环中,节点之间是存在顺序关系的,
* 所以TreeMap的key必须实现Comparator接口
* @author */
public final class KetamaNodeLocator { private TreeMap<Long, Node> ketamaNodes; // 记录所有虚拟服务器节点,为什么是Long类型,因为Long实现了Comparable接口
private HashAlgorithm hashAlg;
private int numReps = 160; // 每个服务器节点生成的虚拟服务器节点数量,默认设置为160 public KetamaNodeLocator(List<Node> nodes, HashAlgorithm alg, int nodeCopies) {
hashAlg = alg;
ketamaNodes = new TreeMap<Long, Node>(); numReps = nodeCopies; // 对所有节点,生成numReps个虚拟结点
for (Node node : nodes) {
// 每四个虚拟结点为一组,为什么这样?下面会说到
for (int i = 0; i < numReps / 4; i++) {
// 为这组虚拟结点得到惟一名称
byte[] digest = hashAlg.computeMd5(node.getName() + i);
/**
* Md5是一个16字节长度的数组,将16字节的数组每四个字节一组,
* 分别对应一个虚拟结点,这就是为什么上面把虚拟结点四个划分一组的原因
*/
for (int h = 0; h < 4; h++) {
// 对于每四个字节,组成一个long值数值,做为这个虚拟节点的在环中的惟一key
long m = hashAlg.hash(digest, h); ketamaNodes.put(m, node);
}
}
}
} /**
* 根据一个key值在Hash环上顺时针寻找一个最近的虚拟服务器节点
* @param k
* @return
*/
public Node getPrimary(final String k) {
byte[] digest = hashAlg.computeMd5(k);
Node rv = getNodeForKey(hashAlg.hash(digest, 0)); // 为什么是0?猜测:0、1、2、3都可以,但是要固定
return rv;
} Node getNodeForKey(long hash) {
final Node rv;
Long key = hash;
//如果找到这个节点,直接取节点,返回
if (!ketamaNodes.containsKey(key)) {
//得到大于当前key的那个子Map,然后从中取出第一个key,就是大于且离它最近的那个key
SortedMap<Long, Node> tailMap = ketamaNodes.tailMap(key);
if (tailMap.isEmpty()) {
key = ketamaNodes.firstKey();
} else {
key = tailMap.firstKey();
}
// For JDK1.6 version
// key = ketamaNodes.ceilingKey(key);
// if (key == null) {
// key = ketamaNodes.firstKey();
// }
} rv = ketamaNodes.get(key);
return rv;
}
}
  KetamaNodeLocator类是实现一致性哈希环的类,记录了所有的服务器节点(虚拟服务器)在环上的位置,以及服务器节点本身的信息(存放在Node中),同时还提供了一个根据key值在Hash环上顺时针寻找一个最近的虚拟服务器节点的方法。
  在多数博客上都有对虚拟服务器节点的使用做出解释,一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题。就是说在物理服务器很少的时候,可能出现服务器节点通过Hash算法集中映射在环的某一部分,导致数据在映射的时候都分布到某一台或几台服务器上,无法达到负载均衡的目的,这也是违背我们使用分布式系统的初衷的。通过将一个物理节点虚拟成多个虚拟节点的方法,能够使得服务器(虚拟的)在Hash环上分布很均匀,避免出现以上的情况。在这里我还要补充一点使用虚拟服务器节点的作用,当一个分布式的集群在正常负载均衡的情况下所有服务器都饱和工作、达到极限值时,我们需要通过增加物理机器的方法来扩展整个分布式系统的性能,让新加入的服务器分担整个分布式系统上的数据压力。假如不使用虚拟节点,新加入的服务器经过Hash算法映射到环上的某一点,它只对顺时针方向上的下一个服务器产生影响,也就是说它只能分担一个服务器上的数据压力,对于其他的服务器,情况仍不容乐观。而使用虚拟节点,我们就能很好的解决这个问题。
  以上hash映射是通过MD5算法实现,MD5算法会产生一个16字节的数组,通过将其切成4段,每一段作为一个hash值生成唯一的标识。下面是hash算法的源码:
 /**
* hash算法,通过MD5算法实现
* MD5算法根据key生成一个16字节的序列,我们将其切成4段,将其中一段作为得到的Hash值
* 在生成虚拟服务器节点中,我们将这四段分别作为四个虚拟服务器节点的唯一标识,即四个hash值
* @author XXX
*/
public enum HashAlgorithm { /**
* MD5-based hash algorithm used by ketama.
*/
KETAMA_HASH; public long hash(byte[] digest, int nTime) {
long rv = ((long) (digest[3+nTime*4] & 0xFF) << 24)
| ((long) (digest[2+nTime*4] & 0xFF) << 16)
| ((long) (digest[1+nTime*4] & 0xFF) << 8)
| (digest[0+nTime*4] & 0xFF); /**
* 实际我们只需要后32位即可,为什么返回一个long类型?
* 因为Long实现了Comparable接口
* Hash环上的节点之间是存在顺序关系的,必须实现Comparable接口
*/
return rv & 0xffffffffL; /* Truncate to 32-bits */
} /**
* Get the md5 of the given key.
*/
public byte[] computeMd5(String k) {
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 not supported", e);
}
md5.reset();
byte[] keyBytes = null;
try {
keyBytes = k.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Unknown string :" + k, e);
} md5.update(keyBytes);
return md5.digest();
}
}

测试:

 /**
* 分布平均性测试
* @author */
public class HashAlgorithmTest { static Random ran = new Random(); // key的数量,key在实际客户端中是根据要存储的值产生的hash序列?
private static final Integer EXE_TIMES = 100000;
// 服务器节点的数量
private static final Integer NODE_COUNT = 5;
// 每个服务器节点生成的虚拟节点数量
private static final Integer VIRTUAL_NODE_COUNT = 160; /**
* 模拟EXE_TIMES个客户端数据存储时选择缓存服务器的情况,
* 得到每个服务器节点所存储的值的数量,从而计算出值在服务器节点的分布情况
* 判断该算法的"性能",正常情况下要求均匀分布
* @param args
*/
public static void main(String[] args) {
HashAlgorithmTest test = new HashAlgorithmTest(); // 记录每个服务器节点所分布到的key节点数量
Map<Node, Integer> nodeRecord = new HashMap<Node, Integer>(); // 模拟生成NODE_COUNT个服务器节点
List<Node> allNodes = test.getNodes(NODE_COUNT);
// 将服务器节点根据Hash算法扩展成VIRTUAL_NODE_COUNT个虚拟节点布局到Hash环上(实际上是一棵搜索树)
// 由KetamaNodeLocator类实现和记录
KetamaNodeLocator locator =
new KetamaNodeLocator(allNodes, HashAlgorithm.KETAMA_HASH, VIRTUAL_NODE_COUNT); // 模拟生成随机的key值(由长度50以内的字符组成)
List<String> allKeys = test.getAllStrings();
for (String key : allKeys) {
// 根据key在Hash环上找到相应的服务器节点node
Node node = locator.getPrimary(key); // 记录每个服务器节点分布到的数据个数
Integer times = nodeRecord.get(node);
if (times == null) {
nodeRecord.put(node, 1);
} else {
nodeRecord.put(node, times + 1);
}
} // 打印分布情况
System.out.println("Nodes count : " + NODE_COUNT + ", Keys count : " + EXE_TIMES + ", Normal percent : " + (float) 100 / NODE_COUNT + "%");
System.out.println("-------------------- boundary ----------------------");
for (Map.Entry<Node, Integer> entry : nodeRecord.entrySet()) {
System.out.println("Node name :" + entry.getKey() + " - Times : " + entry.getValue() + " - Percent : " + (float)entry.getValue() / EXE_TIMES * 100 + "%");
} } /**
* Gets the mock node by the material parameter
*
* @param nodeCount
* the count of node wanted
* @return
* the node list
*/
private List<Node> getNodes(int nodeCount) {
List<Node> nodes = new ArrayList<Node>(); for (int k = 1; k <= nodeCount; k++) {
Node node = new Node("node" + k);
nodes.add(node);
} return nodes;
} /**
* All the keys
*/
private List<String> getAllStrings() {
List<String> allStrings = new ArrayList<String>(EXE_TIMES); for (int i = 0; i < EXE_TIMES; i++) {
allStrings.add(generateRandomString(ran.nextInt(50)));
} return allStrings;
} /**
* To generate the random string by the random algorithm
* <br>
* The char between 32 and 127 is normal char
*
* @param length
* @return
*/
private String generateRandomString(int length) {
StringBuffer sb = new StringBuffer(length); for (int i = 0; i < length; i++) {
sb.append((char) (ran.nextInt(95) + 32));
} return sb.toString();
}
}

==========================神奇的分割线=========================

                           源码请猛戳{ 这里

===========================================================

参考资料:

一致性哈希算法及其在分布式系统中的应用

memcached全面剖析–4. memcached的分布式算法

大型网站技术架构:核心原理与案例分析

一致性哈希Java源码分析的更多相关文章

  1. Java源码分析 | CharSequence

    本文基于 OracleJDK 11, HotSpot 虚拟机. CharSequence 定义 CharSequence 是 java.lang 包下的一个接口,是 char 值的可读序列, 即其本身 ...

  2. Java源码分析:关于 HashMap 1.8 的重大更新(转载)

    http://blog.csdn.net/carson_ho/article/details/79373134 前言 HashMap 在 Java 和 Android 开发中非常常见 而HashMap ...

  3. JAVA源码分析-HashMap源码分析(二)

    本文继续分析HashMap的源码.本文的重点是resize()方法和HashMap中其他的一些方法,希望各位提出宝贵的意见. 话不多说,咱们上源码. final Node<K,V>[] r ...

  4. Java源码分析之LinkedList

    LinkedList与ArrayList正好相对,同样是List的实现类,都有增删改查等方法,但是实现方法跟后者有很大的区别. 先归纳一下LinkedList包含的API 1.构造函数: ①Linke ...

  5. Java源码分析:Guava之不可变集合ImmutableMap的源码分析

    一.案例场景 遇到过这样的场景,在定义一个static修饰的Map时,使用了大量的put()方法赋值,就类似这样-- public static final Map<String,String& ...

  6. 【转】【java源码分析】Map中的hash算法分析

    全网把Map中的hash()分析的最透彻的文章,别无二家. 2018年05月09日 09:08:08 阅读数:957 你知道HashMap中hash方法的具体实现吗?你知道HashTable.Conc ...

  7. JAVA源码分析-HashMap源码分析(一)

    一直以来,HashMap就是Java面试过程中的常客,不管是刚毕业的,还是工作了好多年的同学,在Java面试过程中,经常会被问到HashMap相关的一些问题,而且每次面试都被问到一些自己平时没有注意的 ...

  8. 【Java源码分析】LinkedList类

    LinkedList<E> 源码解读 继承AbstractSequentialList<E> 实现List<E>, Deque<E>, Cloneabl ...

  9. JAVA源码分析------锁(1)

    http://870604904.iteye.com/blog/2258604 第一次写博客,也就是记录一些自己对于JAVA的一些理解,不足之处,请大家指出,一起探讨. 这篇博文我打算说一下JAVA中 ...

随机推荐

  1. android MD5 SHA1

    参考文章: AndroidStudio 中怎样查看获取MD5和SHA1值(应用签名)(https://www.cnblogs.com/zhchoutai/p/7102516.html) 使用 java ...

  2. vue深入了解组件——动态组件&异步组件

    一.在动态组件上使用 keep-alive 我们之前曾经在一个多标签的界面中使用 is 特性来切换不同的组件: <component v-bind:is="currentTabComp ...

  3. eclipse在运行main方法时在console里面报内存溢出的错误解决办法

    修改JVM的配置. window-->preferences-->Java-->installedJres选中使用的jdk/jre版本 点击右边的edit在弹出的对话框中的[Defa ...

  4. spring coud feign

    1. 依赖 <parent> <groupId>org.springframework.boot</groupId> <artifactId>sprin ...

  5. Mysql binlog二进制日志

    Mysql binlog日志有三种格式,分别为Statement,MiXED,以及ROW! 1.Statement:每一条会修改数据的实际原sql语句都会被记录在binlog中. 优点:不需要记录每一 ...

  6. UGUI Auto Layout 自动布局

    Layout Element 首先分配 Minimum Size 如果还有足够空间,分配 Preferred Size 如果还有额外空间,分配 Flexible Size 比较特别的是 Flexibl ...

  7. How to Pronounce INTERNATIONAL

    How to Pronounce INTERNATIONAL Share Tweet Share Tagged With: Dropped T How do you pronounce this lo ...

  8. Kotlin语言学习笔记(4)

    函数 // 函数定义及调用 fun double(x: Int): Int { return 2*x } val result = double(2) // 调用方法 Sample().foo() / ...

  9. 浅谈Spark应用程序的性能调优

    浅谈Spark应用程序的性能调优 :http://geek.csdn.net/news/detail/51819 下面列出的这些API会导致Shuffle操作,是数据倾斜可能发生的关键点所在 1. g ...

  10. Zookeeper—学习笔记(一)

    1.Zookeeper基本功能 (增 删 改 查:注册,监听) 两点: 1.放数据(少量). 2.监听节点.  注意: Zookeeper中的数据不同于数据库中的数据,没有表,没有记录,没有字段: Z ...