要讲一致性Hash原理,先从通常性Hash讲起,其实Hash的本质就是一个长度可变的数组,那为何Hash的时间复杂度是O(1),而其余类型的数据结构查找都是要遍从来,遍历去,即使是树,二叉树,也是要通过几回比对,才能断定查找对象的位置,时间复杂度是O(Log(n)),那为何Hash不用在数组里面遍历呢?node
缘由就在于Hash由存储的对象自己的HashCode以及数组的长度来决定在数组中的位置,这样一看到这两个条件就能够找到对象在数组中的位置而无需去遍历数组,但算出这个位置(即Hash值)在各个版本中是不一样的,如HashMap就是各类位操做.通常咱们本身是用HashCode对数组长度取模来算得对象的Hash值.但数组在位置不够的状况下会进行扩容,HashMap就是在3/4的时候进行扩容,但不管如何,扩容后,数组长度变化就要进行一次rehash,也就是从新计算每一个对象在数组中的位置,即hash值.咱们能够来看一下这段代码,虽然他没有HashMap那么复杂,但原理是同样,只不过计算Hash值的时候只用了最简单的取模.算法
public class SeparateChainingHashTable<T> { private static final int DEFAULT_TABLE_SIZE = 10; private List<T>[] theLists; private int currentSize; //调用带参构造器 public SeparateChainingHashTable() { this(DEFAULT_TABLE_SIZE); } //初始化一个数组,并把数组中每个链表初始化 public SeparateChainingHashTable(int size) { //初始化一个11位的数组 theLists = new LinkedList[nextPrime(size)]; for (int i = 0;i < theLists.length;i++) { theLists[i] = new LinkedList<>(); } } private int myhash(T x) { //取得对象的hash值 int hashVal = x.hashCode(); //hash值对数组长度取模 hashVal %= theLists.length; if (hashVal < 0) { hashVal += theLists.length; } return hashVal; } public void insert(T x) { //从链表数组中取得第该对象哈希值位的链表. List<T> whichList = theLists[myhash(x)]; //若是该链表不包含该对象,则链表添加该对象 if (!whichList.contains(x)) { whichList.add(x); //若是currentSize加1后大于数组的长度,扩容从新计算hash if (++currentSize > theLists.length) rehash(); } } public void remove(T x) { List<T> whichList = theLists[myhash(x)]; if (whichList.contains(x)) { whichList.remove(x); currentSize--; } } public boolean contains(T x) { List<T> whichList = theLists[myhash(x)]; return whichList.contains(x); } public void makeEmpty() { for (int i = 0;i < theLists.length;i++) { theLists[i].clear(); } currentSize = 0; } private void rehash() { List<T>[] oldLists = theLists; //进行一次扩容,扩容后长度为23,可是是一个新的数组 theLists = new List[nextPrime(2 * theLists.length)]; for(int j = 0;j < theLists.length;j++){ //初始化新数组中的每个链表 theLists[j] = new LinkedList<T>(); } //将新数组的currentSize归0 currentSize = 0; //将原有的链表对象放入新数组中,并从新取模计算hash值 for (int i = 0; i < oldLists.length; i++) { for (T item : oldLists[i]) { insert(item); } } } private static int nextPrime(int num) { if (num == 0 || num == 1 || num == 2) { return 2; } if (num % 2 == 0) { num++; } while (!isPrime(num)) { num += 2; } return num; } private static boolean isPrime(int num) { if (num == 2 || num == 3) { return true; } if (num == 1 || num % 2 == 0) { return false; } for (int i = 3; i * i <= num; i += 2) { if (num % i == 0) { return false; } } return true; } public void printTable() { for(int i = 0;i < theLists.length;i++){ System.out.println("-----"); Iterator iterator = theLists[i].iterator(); while(iterator.hasNext()){ System.out.print(iterator.next() + " "); } System.out.println(); } } public static void main(String[] args) { Random random = new Random(); SeparateChainingHashTable<Integer> hashTable = new SeparateChainingHashTable<Integer>(); for (int i = 0; i < 30; i++) { Integer tmp = random.nextInt(30); hashTable.insert(tmp); System.out.printf(tmp + "\t"); } hashTable.printTable(); } }
运行结果:数组
0 17 15 20 14 8 7 2 28 12 10 5 25 11 22 13 9 17 20 8 8 14 28 28 24 4 11 26 9 15 -----
0
-----
24
-----
2 25
-----
26
-----
4
-----
5 28
-----服务器
-----
7
-----
8
-----
9
-----
10
-----
11
-----
12
-----
13
-----
14
-----
15
-----数据结构
-----
17
-----负载均衡
-----dom
-----
20
-----测试
-----
22 大数据
以上有两个数排在一块儿的是由于他们在rehash后有相同的hash值,并被放入链表的第一位和第二位.咱们这里存储的对象是一个LinkedList的链表,而HashMap存储的是一个Map对象,至于你本身要写一个Hash要存储什么对象那是你本身的事.而他们的扩容方式也是不一样的,至于如何扩容那也是你本身的事.this
知道了普通Hash的原理,咱们来看看一致性Hash.一致性Hash是由一个固定长度的Hash环构成,大小为2的32次方.通常用在服务器集群的增删节点的处理上,根据节点名称的Hash值(其分布为[0, 232-1])将服务器节点放置在这个Hash环上,而后根据数据的Key值计算获得其Hash值(其分布也为[0, 232-1]),接着在Hash环上顺时针查找距离这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射查找。(以上斜体红色为次方).这里咱们有一个问题,就是构建一致性Hash环用什么数据结构,难道也要用数组?固然不是,咱们要根据咱们的数据Key值进入Hash环的Hash值来查找服务器节点的Hash值的最短期复杂度来决定,这就一样存在着查找的问题.
首先咱们要对服务器节点的Hash值进行一个存储,是否要排序,如何查找他们最快,是解决这个问题的关键.通常在查找中的时间复杂度以下.
O(1) < O(log2N) < O(n) < O(N * log2N) < O(N2) < O(N3) < 2N < 3N < N!
咱们知道查找最快的是树,比数组,链表都快.因此咱们就选用红黑树来创建这个Hash环,而Java中已经有TreeMap和TreeSet都实现了红黑树.以TreeMap为例,TreeMap自己提供了一个tailMap(T fromKey)方法,支持从红黑树中查找比fromKey大的值的集合,但并不须要遍历整个数据结构。使用红黑树,可使得查找的时间复杂度为O(logN).咱们对ArrayList,LinkedList和TreeMap进行比对
能够看到,数据查找的效率,TreeMap是完胜的,其实再增大数据测试也是同样的,红黑树的数据结构决定了任何一个大于N的最小数据,它都只须要几回至几十次查找就能够查到。查找快,可是插入慢,这是红黑树的特色决定的.为了维护红黑树的平衡性,插入效率,红黑树在三种数据结构里是最差的.
定义出来的Hash环以下
private SortedMap<Long, T> circle = new TreeMap();
重写HashCode的算法
为何要重写HashCode算法,由于Java自己自带的HashCode算法链接太紧密.
public class StringHashCodeTest { public static void main(String[] args) { System.out.println("192.168.0.0:111的哈希值:" + "192.168.0.0:1111".hashCode()); System.out.println("192.168.0.1:111的哈希值:" + "192.168.0.1:1111".hashCode()); System.out.println("192.168.0.2:111的哈希值:" + "192.168.0.2:1111".hashCode()); System.out.println("192.168.0.3:111的哈希值:" + "192.168.0.3:1111".hashCode()); System.out.println("192.168.0.4:111的哈希值:" + "192.168.0.4:1111".hashCode()); } }
运行结果:
192.168.0.0:111的哈希值:1845870087
192.168.0.1:111的哈希值:1874499238
192.168.0.2:111的哈希值:1903128389
192.168.0.3:111的哈希值:1931757540
192.168.0.4:111的哈希值:1960386691
咱们知道咱们的Hash环是2的32次方,而这几个Hash值分布在这个环上面,简直挨的太紧,不利于进入服务器数据的均匀分布,由于进入服务器数据自己的Hash值可能在他们其间的不多不多,要么都进最大的的哈希值服务器,要么都进最小的哈希值服务器.因此咱们要重写HashCode的计算方式使得服务器的Hash值在Hash环中均匀分布.如下是重写的两个计算Hash值的算法.
/** * 使用MD5算法 * @param key * @return */ private static long md5HashingAlg(String key) { MessageDigest md5 = null; try { md5 = MessageDigest.getInstance("MD5"); md5.reset(); md5.update(key.getBytes()); byte[] bKey = md5.digest(); long res = ((long) (bKey[3] & 0xFF) << 24) | ((long) (bKey[2] & 0xFF) << 16) | ((long) (bKey[1] & 0xFF) << 8)| (long) (bKey[0] & 0xFF); return res; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return 0l; } /** * 使用FNV1hash算法 * @param key * @return */ private static long fnv1HashingAlg(String key) { final int p = 16777619; int hash = (int) 2166136261L; for (int i = 0; i < key.length(); i++) hash = (hash ^ key.charAt(i)) * p; hash += hash << 13; hash ^= hash >> 7; hash += hash << 3; hash ^= hash >> 17; hash += hash << 5; return hash; }
虽然咱们但愿重写HashCode算法后,但愿可以在Hash环中均匀分布服务器节点,但依然有可能分布不均匀.示例以下
public class ConsistentHashingWithoutVirtualNode { private static String[] servers = {"192.168.0.0:111", "192.168.0.1:111", "192.168.0.2:111", "192.168.0.3:111", "192.168.0.4:111"}; private static SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>(); static { for (int i = 0; i < servers.length; i++) { int hash = getHash(servers[i]); System.out.println("[" + servers[i] + "]加入集合中, 其Hash值为" + hash); sortedMap.put(hash, servers[i]); } System.out.println(); } private static int getHash(String str) { final int p = 16777619; int hash = (int) 2166136261L; for (int i = 0; i < str.length(); i++) hash = (hash ^ str.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; } private static String getServer(String node) { int hash = getHash(node); Integer i; //取得服务器Key大于传入数据的hash值的全部TreeMap节点 SortedMap<Integer, String> subMap = sortedMap.tailMap(hash); //从新获得的TreeMap得到第一个Key if (subMap.size() == 0) { i = sortedMap.firstKey(); } else { i = subMap.firstKey(); } //获得该Key的服务器IP地址,端口号,即value. return subMap.get(i); } public static void main(String[] args) { String[] nodes = {"127.0.0.1:1111", "221.226.0.1:2222", "10.211.0.1:3333"}; for (int i = 0; i < nodes.length; i++) System.out.println("[" + nodes[i] + "]的hash值为" + getHash(nodes[i]) + ", 被路由到结点[" + getServer(nodes[i]) + "]"); } }
运行结果:
[192.168.0.0:111]加入集合中, 其Hash值为575774686
[192.168.0.1:111]加入集合中, 其Hash值为8518713
[192.168.0.2:111]加入集合中, 其Hash值为1361847097
[192.168.0.3:111]加入集合中, 其Hash值为1171828661
[192.168.0.4:111]加入集合中, 其Hash值为1764547046
[127.0.0.1:1111]的hash值为380278925, 被路由到结点[192.168.0.0:111]
[221.226.0.1:2222]的hash值为1493545632, 被路由到结点[192.168.0.4:111]
[10.211.0.1:3333]的hash值为1393836017, 被路由到结点[192.168.0.4:111]
咱们只有祭出终极必杀——虚拟节点
很明显上例中,有5个服务器节点,可是进入服务器集群的3个数据却有2个分配到了同一个服务器节点上,这分明就是负载不均.
如今咱们将这些实体服务器节点进行虚拟化,给他们创造分身:虚拟节点.将一个物理节点拆分为多个虚拟节点,而且同一个物理节点的虚拟节点尽可能均匀分布在Hash环上。
至于一个物理节点应该拆分为多少虚拟节点,下面能够先看一张图:
横轴表示须要为每台福利服务器扩展的虚拟节点倍数,纵轴表示的是实际物理服务器数。能够看出,物理服务器不多,须要更大的虚拟节点;反之物理服务器比较多,虚拟节点就能够少一些。好比有10台物理服务器,那么差很少须要为每台服务器增长100~200个虚拟节点才能够达到真正的负载均衡。
public class ConsistentHashingWithVirtualNode { private static String[] servers = {"192.168.0.0:111", "192.168.0.1:111", "192.168.0.2:111","192.168.0.3:111", "192.168.0.4:111"}; //真实节点,真实节点将不保存在Hash环中 private static List<String> realNodes = new LinkedList<String>(); //虚拟节点,Hash环 private static SortedMap<Integer, String> virtualNodes = new TreeMap<Integer, String>(); //每一个真实节点对应的虚拟节点数 private static final int VIRTUAL_NODES = 10; static { //添加真实节点 for (int i = 0; i < servers.length; i++) realNodes.add(servers[i]); //添加虚拟节点 for (String str : realNodes) { for (int i = 0; i < VIRTUAL_NODES; i++) { //给虚拟节点命名 String virtualNodeName = str + "&&VN" + String.valueOf(i); //重写Hash算法后的虚拟节点的Hash值 int hash = getHash(virtualNodeName); System.out.println("虚拟节点[" + virtualNodeName + "]被添加, hash值为" + hash); virtualNodes.put(hash, virtualNodeName); } } System.out.println(); } private static int getHash(String str) { final int p = 16777619; int hash = (int) 2166136261L; for (int i = 0; i < str.length(); i++) hash = (hash ^ str.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; } private static String getServer(String node) { int hash = getHash(node); String virtualNode; Integer i; SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash); if (subMap.size() == 0) { i = virtualNodes.firstKey(); virtualNode = virtualNodes.get(i); } else { i = subMap.firstKey(); virtualNode = subMap.get(i); } //返回真实节点的IP,端口,而不是虚拟节点名称 return virtualNode.substring(0, virtualNode.indexOf("&&")); } public static void main(String[] args) { String[] nodes = {"127.0.0.1:1111", "221.226.0.1:2222", "10.211.0.1:3333"}; for (int i = 0; i < nodes.length; i++) System.out.println("[" + nodes[i] + "]的hash值为" + getHash(nodes[i]) + ", 被路由到结点[" + getServer(nodes[i]) + "]"); } }
这样咱们就能够获得密密麻麻的虚拟节点了(这里注意加入Hash环的只有虚拟节点,没有真实节点),运行结果以下:
虚拟节点[192.168.0.0:111&&VN0]被添加, hash值为1686427075
虚拟节点[192.168.0.0:111&&VN1]被添加, hash值为354859081
虚拟节点[192.168.0.0:111&&VN2]被添加, hash值为1306497370
虚拟节点[192.168.0.0:111&&VN3]被添加, hash值为817889914
虚拟节点[192.168.0.0:111&&VN4]被添加, hash值为396663629
虚拟节点[192.168.0.0:111&&VN5]被添加, hash值为1220868525
虚拟节点[192.168.0.0:111&&VN6]被添加, hash值为213398042
虚拟节点[192.168.0.0:111&&VN7]被添加, hash值为1296671064
虚拟节点[192.168.0.0:111&&VN8]被添加, hash值为1718596903
虚拟节点[192.168.0.0:111&&VN9]被添加, hash值为1942098080
虚拟节点[192.168.0.1:111&&VN0]被添加, hash值为1032739288
虚拟节点[192.168.0.1:111&&VN1]被添加, hash值为707592309
虚拟节点[192.168.0.1:111&&VN2]被添加, hash值为302114528
虚拟节点[192.168.0.1:111&&VN3]被添加, hash值为36526861
虚拟节点[192.168.0.1:111&&VN4]被添加, hash值为848442551
虚拟节点[192.168.0.1:111&&VN5]被添加, hash值为779152590
虚拟节点[192.168.0.1:111&&VN6]被添加, hash值为105241177
虚拟节点[192.168.0.1:111&&VN7]被添加, hash值为391408881
虚拟节点[192.168.0.1:111&&VN8]被添加, hash值为1058221668
虚拟节点[192.168.0.1:111&&VN9]被添加, hash值为48793816
虚拟节点[192.168.0.2:111&&VN0]被添加, hash值为1452694222
虚拟节点[192.168.0.2:111&&VN1]被添加, hash值为2023612840
虚拟节点[192.168.0.2:111&&VN2]被添加, hash值为697907480
虚拟节点[192.168.0.2:111&&VN3]被添加, hash值为790847074
虚拟节点[192.168.0.2:111&&VN4]被添加, hash值为2010506136
虚拟节点[192.168.0.2:111&&VN5]被添加, hash值为866437122
虚拟节点[192.168.0.2:111&&VN6]被添加, hash值为149660808
虚拟节点[192.168.0.2:111&&VN7]被添加, hash值为1775912123
虚拟节点[192.168.0.2:111&&VN8]被添加, hash值为663860070
虚拟节点[192.168.0.2:111&&VN9]被添加, hash值为1126545273
虚拟节点[192.168.0.3:111&&VN0]被添加, hash值为891084251
虚拟节点[192.168.0.3:111&&VN1]被添加, hash值为1725031739
虚拟节点[192.168.0.3:111&&VN2]被添加, hash值为1127720370
虚拟节点[192.168.0.3:111&&VN3]被添加, hash值为676720500
虚拟节点[192.168.0.3:111&&VN4]被添加, hash值为2050578780
虚拟节点[192.168.0.3:111&&VN5]被添加, hash值为490504949
虚拟节点[192.168.0.3:111&&VN6]被添加, hash值为2072852996
虚拟节点[192.168.0.3:111&&VN7]被添加, hash值为1058823147
虚拟节点[192.168.0.3:111&&VN8]被添加, hash值为2014386380
虚拟节点[192.168.0.3:111&&VN9]被添加, hash值为1763758471
虚拟节点[192.168.0.4:111&&VN0]被添加, hash值为586921010
虚拟节点[192.168.0.4:111&&VN1]被添加, hash值为184078390
虚拟节点[192.168.0.4:111&&VN2]被添加, hash值为1331645117
虚拟节点[192.168.0.4:111&&VN3]被添加, hash值为918790803
虚拟节点[192.168.0.4:111&&VN4]被添加, hash值为1232193678
虚拟节点[192.168.0.4:111&&VN5]被添加, hash值为1322955826
虚拟节点[192.168.0.4:111&&VN6]被添加, hash值为922655758
虚拟节点[192.168.0.4:111&&VN7]被添加, hash值为1658127198
虚拟节点[192.168.0.4:111&&VN8]被添加, hash值为669639717
虚拟节点[192.168.0.4:111&&VN9]被添加, hash值为938227397
[127.0.0.1:1111]的hash值为380278925, 被路由到结点[192.168.0.1:111]
[221.226.0.1:2222]的hash值为1493545632, 被路由到结点[192.168.0.4:111]
[10.211.0.1:3333]的hash值为1393836017, 被路由到结点[192.168.0.2:111]
由结果咱们能够看出,进入服务器集群的3个数据被分配到了3个不一样的真实服务器节点上面了.