一致性Hash算法(转)

java基础

Posted by TBKK on October 15, 2018

一致性Hash

第一:简单介绍

一致性哈希算法是分布式系统中常用的算法。比如,一个分布式的存储系统,要将对象存储到具体的节点上,如果采用普通的hash方法,将数据映射到具体的节点上,如key%N,N是机器节点数。

1、考虑到比如一个服务器down掉,服务器结点N变为N-1,映射公式必须变为key%(N-1)

2、访问量加重,需要添加服务器结点,N变为N+1,映射公式变为hash(object)%(N+1)

当出现1,2的情况意味着我们的映射都将无效,对服务器来说将是一场灾难,尤其是对缓存服务器来说,因为缓存服务器映射的失效,洪水般的访问都将冲向后台服务器。

第二点:hash算法的单调性

Hash 算法的一个衡量指标是单调性,单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。

consistent hash 也是一种hash 算法,简单的说,在移除 / 添加一个结点时,它能够尽可能小的改变已存在的映射关系,尽可能的满足单调性的要求。

第三点:将对象和服务器结点分别映射到环型空间

通常的一致性哈希做法是将 value 映射到一个 32 位的 key 值,也即是 0~2^32-1 次方的数值空间;我们可以将这个空间想象成一个首( 0 )尾( 2^32-1 )相接的圆环。

我们可以通过hash函数将我们的key映射到环型空间中,同时根据相同的哈希算法把服务器也映射到环型空间中,顺便提一下服务器或者某个计算节点的 hash 计算,一般的方法可以使用机器的 IP 地址或者机器名作为 hash 输入。

第四点:将对象映射到服务器

在这个环形空间中,如果沿着顺时针方向从对象的 key 值出发,直到遇见一个 服务器结点,那么就将该对象存储在这个服务器结点上,因为对象和服务器的hash 值是固定的,因此这个 cache 必然是唯一和确定的。

这时候考察某个服务器down机或者需要添加服务器结点,也就是移除和添加的操作,我们只需要几个对象的映射。

下面来看一下不带虚拟节点的一致性Hash算法的Java代码实现:

/**
 * 不带虚拟结点的一致性Hash算法
 *
 */
public class ConsistentHashWithoutVN {

	/**
	 * 待加入Hash环的服务器列表
	 */
	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" };

	/**
	 * key表示服务器的hash值,value表示服务器的名称
	 */
	private static SortedMap<Integer, String> sortedMap = new TreeMap<>();

	/**
	 * 程序初始化,将所有服务器加入集合
	 */
	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]);
		}
	}

	/**
	 * 使用FNV1_32_HASH算法计算hash值
	 * @param str
	 * @return
	 */
	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 matchServer(String node) {
		// 待路由结点的Hash值
		int hash = getHash(node);
		// 得到大于该Hash值的子Map
		SortedMap<Integer, String> subMap = sortedMap.tailMap(hash);
		// 顺时针的第一个Key
		Integer i = subMap.firstKey();
		// 返回路由到的服务器名称
		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]) + ",被路由到的服务器为[" 
		+ matchServer(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]

第五点:虚拟结点

Hash 算法的另一个指标是平衡性 (Balance)。平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。

对于上述的做法,可能导致某些对象都映射到某个服务器,使得分布不平衡。为此可以采用“虚拟结点”的做法。

“虚拟结点”( virtual node )是实际节点在 hash 空间的复制品,一实际结点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以 hash 值排列。引入“虚拟结点”会让我们的映射分布更为平衡一些。

引入“虚拟结点”前: Hash(“192.168.1.1”);

引入“虚拟结点”后: Hash(“192.168.1.1#1”); Hash(“192.168.1.1#2”);

下面来看一下带虚拟节点的一致性Hash算法的Java代码实现:

/**
 * 带虚拟结点的一致性Hash算法
 *
 */
public class ConsistentHashWithVN {
	/**
	 * 待加入Hash环的服务器列表
	 */
	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" };

	/**
	 * 真实结点列表,考虑到服务器上线、下线的场景,即添加、删除的场景会比较频繁,这里使用LinkedList会更好
	 */
	private static List<String> realNodes = new LinkedList<>();

	/**
	 * key表示虚拟结点服务器的hash值,value表示虚拟结点服务器的名称
	 */
	private static SortedMap<Integer, String> virtualNodes = new TreeMap<>();

	/**
	 * 虚拟结点数目(一个真实结点对应VN_SUM个虚拟结点)
	 */
	private static final int VN_SUM = 5;

	/**
	 * 加所有服务器加入集合
	 */
	static {
		for (int i = 0; i < servers.length; i++) {
			realNodes.add(servers[i]);
		}

		for (String str : realNodes) {
			for (int i = 0; i < VN_SUM; i++) {
				String virtualNodeName = str + "&VN" + String.valueOf(i);
				int hash = getHash(virtualNodeName);
				System.out.println("虚拟节点[" + virtualNodeName + "]被添加, hash值为" + hash);
				virtualNodes.put(hash, virtualNodeName);
			}
		}
		System.out.println("\n===========路由映射==============\n");
	}

	/**
	 * 使用FNV1_32_HASH算法计算hash值
	 * 
	 * @param str
	 * @return
	 */
	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 matchServer(String node) {
		// 待路由结点的Hash值
		int hash = getHash(node);
		// 得到大于该Hash值的子Map
		SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);
		// 顺时针的第一个Key
		Integer i = subMap.firstKey();
		// 截取
		String virtualNode = subMap.get(i);
		
		// 返回路由到的服务器名称
		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", "112.74.15.218:80" };
		for (int i = 0; i < nodes.length; i++) {
			System.out.println(
					"[" + nodes[i] + "]的hash值为" + getHash(nodes[i]) + ",被路由到的服务器为[" + matchServer(nodes[i]) + "]");
		}
	}
}

关注一下运行结果:

虚拟节点[192.168.0.0:111&VN0]被添加, hash值为62550928
虚拟节点[192.168.0.0:111&VN1]被添加, hash值为45670134
虚拟节点[192.168.0.0:111&VN2]被添加, hash值为1069081239
虚拟节点[192.168.0.0:111&VN3]被添加, hash值为681260483
虚拟节点[192.168.0.0:111&VN4]被添加, hash值为345193220
虚拟节点[192.168.0.1:111&VN0]被添加, hash值为1014794997
虚拟节点[192.168.0.1:111&VN1]被添加, hash值为314112378
虚拟节点[192.168.0.1:111&VN2]被添加, hash值为1764217630
虚拟节点[192.168.0.1:111&VN3]被添加, hash值为1754008301
虚拟节点[192.168.0.1:111&VN4]被添加, hash值为1013081826
虚拟节点[192.168.0.2:111&VN0]被添加, hash值为1936519782
虚拟节点[192.168.0.2:111&VN1]被添加, hash值为1962355349
虚拟节点[192.168.0.2:111&VN2]被添加, hash值为1051508275
虚拟节点[192.168.0.2:111&VN3]被添加, hash值为1487794011
虚拟节点[192.168.0.2:111&VN4]被添加, hash值为1010967116
虚拟节点[192.168.0.3:111&VN0]被添加, hash值为1671479534
虚拟节点[192.168.0.3:111&VN1]被添加, hash值为803892279
虚拟节点[192.168.0.3:111&VN2]被添加, hash值为1986618297
虚拟节点[192.168.0.3:111&VN3]被添加, hash值为1068919486
虚拟节点[192.168.0.3:111&VN4]被添加, hash值为454720555
虚拟节点[192.168.0.4:111&VN0]被添加, hash值为232783560
虚拟节点[192.168.0.4:111&VN1]被添加, hash值为1097591827
虚拟节点[192.168.0.4:111&VN2]被添加, hash值为812889841
虚拟节点[192.168.0.4:111&VN3]被添加, hash值为1338995023
虚拟节点[192.168.0.4:111&VN4]被添加, hash值为1008393313

===========路由映射==============

[127.0.0.1:1111]的hash值为380278925,被路由到的服务器为[192.168.0.3:111]
[221.226.0.1:2222]的hash值为1493545632,被路由到的服务器为[192.168.0.3:111]
[10.211.0.1:3333]的hash值为1393836017,被路由到的服务器为[192.168.0.2:111]
[112.74.15.218:80]的hash值为51269059,被路由到的服务器为[192.168.0.0:111]