一致性Hash算法代码实现与标准差测试

为什么hash环长度范围是2"32 -1

​ 一致性hash最初是用来解决ip映射问题;IP是32位,IP地址中间用 . 号隔开的每个数的取值范围是0—255,也就是256个数字,是2的8次方,用2进制表示即为8位,一共4个数字,即为32位

如何构建2的32次方hash环

​ 首先我们得怎么构造这个2的32次方的hash环,方法有很多,我这里就直接推荐使用TreeMap这个数据结构,因为TreeMap底层是使用了红黑树结构来存储实体对象的,时间复杂度在O(logN),效率较高

hash算法的选择,保证散列后节点分布式均匀

​ 我们在选择Hash算法上也需要选好,要尽可能的打散开,如果考虑简单的String.HashCode()方法,这个算法的缺点是,相似的字符串如N1(10.0.0.0:91001),N2(10.0.0.0:91002),N3(10.0.0.0:91003),哈希值也很相近,造成的结果是节点在Hash环上分布很紧密,导致大部分Key值落到了N0上,节点资源分布不均。一般我们采用FNV1_32_HASH、KETAMA_HASH等算法,KETAMA_HASH是MemCache集群默认的实现方法,这些算法效果要好得多,会使N0,N1,N2的Hash值更均匀的分布在环上

Ketama算法

Ketama 算法:将它称之为哈希算法其实不太准确,称之为一致性哈希算法可能更为合适,其他的哈希算法有通用的一致性哈希算法实现,只不过是替换了哈希方式而已,但 Ketama 是一整套的流程

算法代码

下面是简单写的一致性hash算法,md5散列代码和标准差测试代码,直接引用了几位能人之手,如下会标明出处,见谅。

  • 一致性hash主算法
public class ConsistentHashingVirtualNode {
    
    

    /**
     * 虚拟节点树
     */
    private SortedMap<Integer,String> vNodes = new TreeMap();

    /**
     * 真实节点
     */
    private LinkedList<String> realNodes;

    /**
     * 一个真实节点对应的虚拟节点数量
     */
    private int virtualNums;

    /**
     * 虚拟节点连接符
     */
    private static final String VIRTUAL_NODE_SUFFIX = "-";

    /**
     * 构造器初始化hash环
     * @param nodes
     * @param vn
     */
    public ConsistentHashingVirtualNode(LinkedList<String> nodes,int vn){
    
    
        this.realNodes = nodes;
        this.virtualNums = vn;
        initP2Vmapping();
    }

    /**
     * 构建hash环主方法,初始化虚拟节点与物理节点的映射关系;
     * 映射的虚拟节点采用TreeMap排序,利用二叉树特性方便查找节点;
     * map中key存储虚拟节点hash值,value存储对应的实体节点;虚拟节点的hash值来自于实体节点字符拼接;
     */
    private void initP2Vmapping(){
    
    

        for(String node:realNodes){
    
    
            for( int i=0;i< virtualNums;i++ ){
    
    
                String vName = getVirtualNodeNameByIndex(node,i);
                vNodes.put(hash(vName),node);
            }
        }

    }

    /**
     * 拼接虚机节点
     * @param nodeName
     * @param index
     * @return
     */
    private String getVirtualNodeNameByIndex(String nodeName, int index){
    
    
        return new StringBuffer(nodeName)
                .append(VIRTUAL_NODE_SUFFIX)
                .append(index)
                .toString();
    }

    /**
     * hash算法,使用MD5散列。
     * 这里直接拿了 https://github.com/itisaid/Doris/blob/master/common/doris.common/doris.algorithm/src/main/java/com/alibaba/doris/algorithm/util/MD5Util.java 的代码;
     * @param k
     * @return
     */
    private int hash(String k) {
    
    
//        String s = MD5Util.md5( k );
//        return s.hashCode();
        return MD5Util.md5HashCode(k);
    }

    /**
     * 定位被路由到的实际节点
     * @param p
     * @return
     */
    public String locate(String p){
    
    

        SortedMap<Integer,String> toTailed =  vNodes.tailMap(hash(p));
        Integer key = toTailed.isEmpty()?vNodes.firstKey():toTailed.firstKey();
//        System.out.println("virtual-node hash("+hash(p)+"): "+p+" -> realNode hash("+hash(vNodes.get(key))+"):"+vNodes.get(key));
        return vNodes.get(key);

    }

    public static void main(String[] args) {
    
    

        LinkedList<String> realNodes = new LinkedList<>();
        realNodes.add("192.168.1.1");
        realNodes.add("192.168.1.3");
        realNodes.add("192.168.1.5");
        realNodes.add("192.168.1.7");
        realNodes.add("192.168.1.9");

        ConsistentHashingVirtualNode consis
                = new ConsistentHashingVirtualNode(realNodes,12);
        System.out.println(consis.locate("1900000000000"));
    }

}
  • MD5散列算法

    public class MD5Util {
          
          
    
        private static MessageDigest md5Digist;
        private static final char[]  hex = {
          
           '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
    
        static {
          
          
            try {
          
          
                md5Digist = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException e) {
          
          
                e.printStackTrace();
            }
        }
    
        /*
         * 构造md5 String
         * @param key
         * @return
         */
        public static String md5(String key) {
          
          
            if (key == null) {
          
          
                key = "";
            }
            try {
          
          
                MessageDigest messageDigest = (MessageDigest) md5Digist.clone();
                messageDigest.update(key.getBytes());
                byte[] b = messageDigest.digest();
                char[] charArr = new char[32];
                for (int i = 0; i < b.length; i++) {
          
          
                    charArr[i * 2] = hex[b[i] >>> 4 & 0xF];
                    charArr[i * 2 + 1] = hex[b[i] & 0xF];
                }
                return new String(charArr);
    
            } catch (CloneNotSupportedException e) {
          
          
                throw new RuntimeException("md5:" + e,e);
            }
    
        }
    
        /**
         * 使用MD5散列的hash code
         *
         * @param key
         * @return
         */
        public static int md5HashCode(String key) {
          
          
            md5Digist.reset();
            md5Digist.update(key.getBytes());
            byte[] b = md5Digist.digest();
            int rv = ((int) (b[3] & 0xFF) << 24) | ((int) (b[2] & 0xFF) << 16) | ((int) (b[1] & 0xFF) << 8) | (b[0] & 0xFF);
            return rv > 0 ? rv : -rv;
        }
    
    }
    
  • 标准差算法工具

    public class StatisticsUtil {
          
          
    
        // 方差 s^2=[(x1-x)^2 +...(xn-x)^2]/n
        public static double variance(Long[] x) {
          
          
            int m = x.length;
            double sum = 0;
            for (int i = 0; i < m; i++) {
          
          // 求和
                sum += x[i];
            }
            double dAve = sum / m;// 求平均值
            double dVar = 0;
            for (int i = 0; i < m; i++) {
          
          // 求方差
                dVar += (x[i] - dAve)* (x[i] - dAve);
            }
            return dVar / m;
        }
    
        // 标准差σ=sqrt(s^2)
        public static double standardDeviation(Long[] x) {
          
          
            int m = x.length;
            double sum = 0;
            for (int i = 0; i < m; i++) {
          
          // 求和
                sum += x[i];
            }
            double dAve = sum / m;// 求平均值
            double dVar = 0;
            for (int i = 0; i < m; i++) {
          
          // 求方差
                dVar += (x[i] - dAve)* (x[i] - dAve);
            }
            return Math.sqrt(dVar / m);
        }
    
    }
    
  • 单元测试

    public class ConsistentHashTest {
          
          
    
        /**
         * 构建实体ips
         */
        static String[] ips = {
          
          
                "192.111.1.0","192.111.1.2","192.111.1.3",
                "192.172.2.2","192.172.1.2", "192.172.3.2",
                "192.168.3.2","192.168.1.2","192.168.1.2",
                "192.117.4.211"}; // 10 台随机 ip
    
    
        private void testDistribution(int num) {
          
          
    
            /**
             * 实体节点加个端口|无相关
             */
            LinkedList<String> realNodes = new LinkedList<>();
            for (String ip : ips) {
          
          
                realNodes.add(ip+":8080");
            }
            ConsistentHashingVirtualNode chloadBalance
                    = new ConsistentHashingVirtualNode(realNodes,100000);
    
            // 构造 10000 随机请求
            List<String> invocations = new ArrayList<>();
            for (int i = 0; i < 10000; i++) {
          
          
                invocations.add(UUID.randomUUID().toString());
            }
    
            // 统计分布
            AtomicLongMap<String> atomicLongMap = AtomicLongMap.create();
            for (String server : realNodes) {
          
          
                atomicLongMap.put(server, 0);
            }
            for (String invocation : invocations) {
          
          
                String selectedServer = chloadBalance.locate(invocation);
                atomicLongMap.getAndIncrement(selectedServer);
            }
    
    //        System.out.println("10个物理节点,100w个虚拟节点,10000次随机请求,第"+num+"次测试方差:"+StatisticsUtil.variance(atomicLongMap.asMap().values().toArray(new Long[]{})));
            System.out.println("10个物理节点,100w个虚拟节点,10000次随机请求,第"+num+"次测试标准差:"+StatisticsUtil.standardDeviation(atomicLongMap.asMap().values().toArray(new Long[]{
          
          })));
            //https://www.cnkirito.moe/spring-security-6/
        }
    
        @Test
        public void testRange(){
          
          
            for(int i=1;i<=20;i++){
          
          
                testDistribution(i);
            }
    
        }
    }
    

测试结果

在这里插入图片描述

引用内容

猜你喜欢

转载自blog.csdn.net/weiyi_world/article/details/107217853