在工作中常遇到分库分表或者分布式结点分布的问题,保证均衡,可扩展的分配策略很重要,分配策略一般采用hash算法,一般都是余数等策略,这种hash算法在结点固定的时候会有比较均衡的分布,但是碰到需要扩展结点就比较难处理,涉及迁移的数据特别多,所以引入一致性哈希算法

一致性算法能解决什么问题?

  解决了普通余数Hash算法伸缩性差的问题,可以保证在服务器上线或下线变更时尽量有多的请求命中原来路由到的服务器

算法的具体原理:

  先构造一个长度为232的整数环(这个环被称为一致性Hash环),根据节点名称的Hash值(其分布为[0, 232-1])将服务器节点放置在这个Hash环上,然后根据数据的Key值计算得到其Hash值(其分布也为[0, 232-1]),接着在Hash环上顺时针查找距离这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射查找。

实现:

  1、数据结构选取

    根据原理我们需要找到一个合适的数据结构存放232的数据,因为这里的查询特别多,所以尽可能的选用稳定、查找时间复杂度低的数据结构,这里采用红黑树来存储(各数据结构的对比参考其他文档),我们主要是采用他的TreeMap方法,因为他本身提供了tailMap(K fromKey)方法,支持从红黑树中查找比fromKey大的值的集合,但并不需要遍历整个数据结构,这样在集合中取第一个就能实现顺时针查找最近结点的需求

  2、Hash算法

    string的hashcode方法在一致性hash算法中实用价值较低,因为遇到连续的key时hash结果比较集中,做不到均衡,需要重新找个算法计算Hash值,这里算法比较多比如CRC32_HASH、FNV1_32_HASH、KETAMA_HASH等,这里实用FNV1_32_HASH算法(其他hash算法对比参考其他文档)

  3、增加虚拟结点

    在原有的负载均衡分布中增加新的结点,势必会导致平衡破坏,导致新增结点和该结点后一个结点的请求少于其他结点,这种可以增加虚拟结点来解决。原理可以理解为对于新增的结点,把他拆分为多个虚拟结点,然后把这些虚拟结点尽量分表到hash环上,这样可以做到最大程度的解决结点变更导致的负责不均衡问题,但是一个物理结点应该分为多少个虚拟结点(这个问题待研究)

  4、具体代码实现

  1 package com.lexin.hash;
  2 
  3 import java.util.SortedMap;
  4 import java.util.TreeMap;
  5 
  6 /**
  7  * 
  8  * 带虚拟节点的一致性Hash算法
  9  * 〈功能详细描述〉
 10  * @author handyliu
 11  * @version 2018-2-27
 12  * @see ConsistentHashArithmetic
 13  * @since
 14  */
 15 public class ConsistentHashArithmetic {
 16     /**
 17      * 物理的服务器列表
 18      * 这些列表就是要加入到Hash环的服务器列表
 19      */
 20     private static String[] servers = {"192.168.0.0:3306", "192.168.0.1:3306", "192.168.0.2:3306",
 21             "192.168.0.3:3306", "192.168.0.4:3306", "192.168.0.5:3306"};
 22     
 23 
 24     /**
 25      * 虚拟节点使用TreeMap,key表示虚拟节点的hash值,value表示虚拟节点的名称
 26      */
 27     private static SortedMap<Integer, String> virtualNodes = new TreeMap<Integer, String>();
 28     
 29     /**
 30      * 每个物理结点对应的虚拟节点的数目
 31      */
 32     private static final int VIRTUAL_NODES = 5;
 33     
 34     /**
 35      * ConsistentHashArithmetic的静态初始化,把结点放到结点列表中
 36      */
 37     static {
 38     // 先把原始的服务器添加到真实结点列表中
 39         for (int i = 0; i < servers.length; i++){
 40             for (int j = 0; j < VIRTUAL_NODES; j++){
 41                 String virtualNodeName = servers[i] + "&Node" + String.valueOf(i);
 42                 int hashValue = getHashValue(virtualNodeName);
 43                 virtualNodes.put(hashValue, virtualNodeName);
 44                 System.out.println("虚拟节点[" + virtualNodeName + "]已添加到hash环, hash值为:" + hashValue);
 45             }
 46         }
 47     }
 48     
 49     /**
 50      * 使用FNV1_32_HASH算法计算Hash值
 51      */
 52     private static int getHashValue(String str){
 53         final int p = 16777619;
 54         int hash = (int)2166136261L;
 55         for (int i = 0; i < str.length(); i++)
 56             hash = (hash ^ str.charAt(i)) * p;
 57         hash += hash << 13;
 58         hash ^= hash >> 7;
 59         hash += hash << 3;
 60         hash ^= hash >> 17;
 61         hash += hash << 5;
 62         
 63         if (hash < 0)
 64             hash = Math.abs(hash);
 65         return hash;
 66     }
 67     
 68     /**
 69      * 得到应当路由到的结点
 70      */
 71     private static String getServerNode(String splitKey){
 72         //需要分配的key值
 73         int hash = getHashValue(splitKey);
 74         // 获取大于该Hash值的所有Map,直接使用tailMap方法
 75         SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);
 76         String virtualNode;  
 77         if(subMap.isEmpty()){  
 78            //如果没有比该key的hash值大的,则从第一个node开始  
 79            Integer i = virtualNodes.firstKey();  
 80            //返回对应的服务器  
 81            virtualNode = virtualNodes.get(i);  
 82         }else{  
 83            //第一个Key就是顺时针过去离node最近的那个结点  
 84            Integer i = subMap.firstKey();  
 85            //返回对应的服务器  
 86            virtualNode = subMap.get(i);  
 87         }  
 88         //virtualNode虚拟节点名称要截取一下  
 89         if(!"".equals(virtualNode)){  
 90             return virtualNode.substring(0, virtualNode.indexOf("&"));  
 91         }  
 92         return null;  
 93     }
 94     
 95     public static void main(String[] args){
 96         String[] uids = {"100","199", "10000", "19999", "1000000", "1999999", "100000000",  "199999999"};
 97         for (int i = 0; i < uids.length; i++){
 98             System.out.println("[" + uids[i] + "]的hash值为" + getHashValue(uids[i]) + ", 被路由到结点[" + getServerNode(uids[i]) + "]");
 99         }            
100     }
101 }

 

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

我不能保证写的每个地方都是对的,但是至少能保证不复制、不黏贴,保证每一句话、每一行代码都经过了认真的推敲、仔细的斟酌。每一篇文章的背后,希望都能看到自己对于技术、对于生活的态度。

学习是一种信仰。面对压力,挑灯夜战、不眠不休;面对困难,迎难而上、永不退缩。

我是一个纯粹的程序员。

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