对于一致性hash算法,我的理解是它主要是降低了集群扩缩容时对原有路由信息的影响,下面是根据其思想进行的简单实现(java)
代码主要是借鉴的这个网址:https://www.cnblogs.com/xrq730/p/5186728.html ,感谢作者。
主要变动:
1、不带虚拟节点的实现中,加了对子map为空的判断
2、带虚拟节点的实现中,替换了虚拟节点的生成方式,采用了等距划分hash环的方式, 避免了添加后缀后hash仍然比较集中的情况
class ConsistentHashingWithoutVirtualNode { /** * 待添加入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<Integer, String>(); /** * 程序初始化,将所有的服务器放入sortedMap中 */ 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(); } /** * 使用FNV1_32_HASH算法计算服务器的Hash值,这里不使用重写hashCode的方法,最终效果没区别 */ 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; // 如果算出来的值为负数则取其绝对值 return Math.abs(hash); } /** * 得到应当路由到的结点 */ private static String getServer(String node) { // 得到带路由的结点的Hash值 int hash = getHash(node); // 得到大于该Hash值的所有Map SortedMap<Integer, String> subMap = sortedMap.tailMap(hash); System.out.println(subMap); // 第一个Key就是顺时针过去离node最近的那个结点 Integer i = (subMap.isEmpty()) ? sortedMap.firstKey() : subMap.firstKey(); // 返回对应的服务器名称 return sortedMap.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", "l3hfgrtldk"}; for (int i = 0; i < nodes.length; i++) { System.out.println("===="); System.out.println("[" + nodes[i] + "]的hash值为" + getHash(nodes[i]) + ", 被路由到结点[" + getServer(nodes[i]) + "]"); } } } class ConsistentHashingWithVirtualNode { /** * 待添加入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<String>(); /** * 虚拟节点,key表示虚拟节点的hash值,value表示虚拟节点的名称 */ private static SortedMap<Integer, String> virtualNodes = new TreeMap<Integer, String>(); /** * 等距划分hash环 * */ public static int[] getSplitNum(int baseNum, int n) { int distince = Integer.MAX_VALUE / n; int[] res = new int[n]; for (int i = 0; i < n; i++) { res[i] = baseNum + i * distince; if (res[i] < 0) { res[i] += Integer.MAX_VALUE; } } return res; } /** * 虚拟节点的数目,这里写死,为了演示需要,一个真实结点对应5个虚拟节点 */ private static final int VIRTUAL_NODES = 5; static { // 先把原始的服务器添加到真实结点列表中 for (int i = 0; i < servers.length; i++) realNodes.add(servers[i]); for (String str : realNodes) { int realHash = getHash(str); int[] splitNums = getSplitNum(realHash, VIRTUAL_NODES); for (int vHash : splitNums) { virtualNodes.put(vHash, str); System.out.println("虚拟节点[" + str + "]被添加, hash值为" + vHash); } System.out.println("=="); } } /** * 使用FNV1_32_HASH算法计算服务器的Hash值,这里不使用重写hashCode的方法,最终效果没区别 */ 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; return (hash < 0) ? -hash : hash; } /** * 得到应当路由到的结点 */ private static String getServer(String node) { // 得到带路由的结点的Hash值 int hash = getHash(node); // 得到大于该Hash值的所有Map SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash); // 第一个Key就是顺时针过去离node最近的那个结点 Integer i = (subMap.isEmpty()) ? virtualNodes.firstKey() : subMap.firstKey(); return virtualNodes.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]) + "]"); int[] splitNum = getSplitNum(500000000, 5); System.out.println(Arrays.toString(splitNum)); System.out.println(splitNum[0] - splitNum[4]); System.out.println(splitNum[0] - splitNum[1]); } }