真的是最全的一致性hash环讲解了

一致性哈希(哈希环)解决数据分布问题 哈希算法是程序开发过程中最广泛接触到的的算法之一,典型的应用有安全加密、数据校验、唯一标识、散列函数、负载均衡、数据分片、分布式存储。前些天刚接触到夜莺项目,听介绍目前V6版本集群方式部署,告警生成是通过一致性哈希(哈希环)去分片处理,不过我细想一下,对一致性哈希这个知识点好像了解过,但是又没太深印象,说不出具体是什么原理,怎么用,有哪些注意的地方。本文简单记录,希望也能给其他人作为初步了解所用。 阅读详情

一致性 hash 环

最近做项目 做了一个分发器 ,需要 根据请求携带的参数 把请求分发到 不同的服务器上面,最终我选择使用 一致性hash 环 来实现 ,本篇 就主要讲解一下 一致性hash环 它的基本原理

概述

一致性hash算法 由于 均衡性 持久性的映射特点 被广泛应用于负载均衡领域,比如 nginx 、dubbo 、等等 内部都有一致性hash 的实现 ,比如 dubbo ,当你调用rpc 接口的时候,如果有2个提供者,那么你可以通过配置 让其调用通过 一致性hash 进行计算 然后分发到具体的 某个实例接口上 。

1.hash算法 在负载均衡中的问题

先来看看普通的hash算法的特点,普通的hash算法就是把一系列输入 打散成随机的数据,负载均衡就是利用这一点特性,对于大量请求调用,通过一定的 hash将它们均匀地散列,从而实现压力平均化

如果上面图中 key作为缓存的key ,node中寸入该key对应的 value,就是一个 简单的分布式缓存系统了。

问题:可以看出 当 N 节点数发生变化的时候 之前所有的 hash映射几乎全部失效,如果集群是无状态的服务 倒是没什么事情,但是如果是 分布式缓存这种功能,比如 映射的key1 原本是去 node1上查询 缓存的value1, 但是当N节点变化后 hash后的 key1 可能去了 node2 这样 就产生了致命的问题。。

2.一致性 hash 算法

一致性hash算法就是来解决 上面的问题

2.1 特点 (重要)

下面说明 一致性hash算法的 2个 重要的特点

  • 平衡性
  • 平衡性是指哈希的结果能够尽可能分布到所有的缓冲空间中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。
  • 单调性
  • 单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲区加入到系统中,那么哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲区中去,而不会被映射到旧的缓冲集合中的其他缓冲区。 简单的哈希算法往往不能满足单调性的要求

2.2 原理

一致性哈希将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-2^32-1(即哈希值是一个32位无符号整形),整个哈希空间环如下:

就是所有的输入值都被映射到 0-2^32-1 之间,组成一个圆环

下一步将各个服务器使用Hash进行一个哈希, 具体可以选择服务器的ip或主机名或者其他业务属性作为关键字 进行哈希,这样每台机器就能确定其在哈希环上的位置,这里假设将上文中四台服务器使用ip地址哈希后在环空间的位置如下:

如果服务器数量不多且个数相对稳定,我们也可以手动设置这些服务器的位置,如设A在2

30-1位置,B在2

31-1位置,C在3*2

30-1位置,D在2

32-1位置,则hash值为02^30-1的数据存储在A中,hash值为2^30-12^31-1的数据存储在B中,以此类推。

**定位数据存储的方法: **将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。可以为这些服务器维护一条二分查找树,定位服务器的过程就是在二分查找树中找到好比其大的节点。

例如我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:

2.3 一致性hash 优点

如上图,假如当节点A宕机了,那么只会影响 objectA 它会被重新映射到 NodeB节点,其他的ObjectB ObjectC ObjectD 都不会受到影响,大大提高了容错性和稳定性

2.4 一致性hash存在的问题

2.3.1 数据分布不均匀

当节点Node很少的时候 比如2台机器,那么 必然造成大量数据集中在NodeA,少量在NodeB

2.3.2 雪崩

当某个节点宕机后,原本属于它的请求 都会被重新hash 映射到 下游节点,会突然造成下游节点压力过大 有可能也会造成 下游节点宕机,从而形成雪崩 这是致命的

为此 引入了 虚拟节点来解决上面两个问题

3.带虚拟节点的一致性hash

就是会在圆环上 根据Node 节点 生成很多的虚拟节点 分布在圆环上,这样当 某个 节点挂了后 原本属于它的请求,会被均衡的分布到 其他节点上 降低了产生雪崩的情况,也解决了 节点数少导致 请求分布不均的请求

即对每一个服务节点计算多个哈希(可以用原节点key+"##xxxk"作为每个虚拟节点的key,然后求hashcode),每个计算结果位置都放置一个此服务节点,称为虚拟节点。 具体做法可以在服务器ip或主机名的后面增加编号来实现。

看上图,此时如果 group3节点挂了,那么请求会被均分到 group2 和 group1上面 ,到此 一致性hash的正确生产的使用方式讲解完了 下面来看看一个案例代码。

4. 代码测试

可供选择的有很多,memcached官方使用了基于md5的KETAMA算法,但这里处于计算效率的考虑,使用了FNV1_32_HASH算法,如下:

public class HashUtil {
    /**
     * 计算Hash值, 使用FNV1_32_HASH算法
     * @param str
     * @return
     */
    public 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;
    }
}
package com.weareint.dispatchservice.hashloop;

import org.springframework.stereotype.Component;

import java.util.*;

/**
 *
 *
 * <pre>
 *  一致性 hash 虚拟 环
 * </pre>
 *
 * @author johnny
 * @date 2021-08-26 9:22 上午
 */
@Component
public class HashVirtualNodeCircle {

    /** 真实集群列表 */
    private static List<String> instanceNodes;

    /** 虚拟节点映射关系 */
    private static SortedMap<Integer, String> virtualNodes = new TreeMap<>();

    /** 虚拟节点数 */
    private static final int VIRTUAL_NODE_NUM = 1000;

    /** 刷新 服务实例 */
    public void refreshVirtualHashCircle(HashCircleInstanceNodeBuild build) {
        // 当集群变动时,刷新hash环,其余的集群在hash环上的位置不会发生变动
        virtualNodes.clear();
        // 获取 最新节点
        instanceNodes = build.instanceNodes();
        // 将虚拟节点映射到Hash环上
        for (String realInstance : instanceNodes) {
            for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
                String virtualNodeName = getVirtualNodeName(realInstance, i);
                int hash = HashUtils.getHash(virtualNodeName);
                System.out.println("[" + virtualNodeName + "] launched @ " + hash);
                virtualNodes.put(hash, virtualNodeName);
            }
        }
    }

    private static String getVirtualNodeName(String realName, int num) {
        return realName + "&&VN" + num;
    }

    private static String getRealNodeName(String virtualName) {
        return virtualName.split("&&")[0];
    }

    private static String getServer(String widgetKey) {
        int hash = HashUtils.getHash(widgetKey);
        // 只取出所有大于该hash值的部分而不必遍历整个Tree
        SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);
        String virtualNodeName;
        if (subMap.isEmpty()) {
            // hash值在最尾部,应该映射到第一个group上
            virtualNodeName = virtualNodes.get(virtualNodes.firstKey());
        } else {
            virtualNodeName = subMap.get(subMap.firstKey());
        }
        return getRealNodeName(virtualNodeName);
    }

    public static void main(String[] args) {
        HashVirtualNodeCircle hashVirtualNodeCircle = new HashVirtualNodeCircle();
        hashVirtualNodeCircle.refreshVirtualHashCircle(
                new HashCircleInstanceNodeBuild() {
                    @Override
                    public List<String> instanceNodes() {
                        LinkedList<String> nodes = new LinkedList<>();
                        nodes.add("192.168.11.23:8090");
                        nodes.add("192.168.11.23:8093");
                        nodes.add("192.168.11.23:8094");
                        return nodes;
                    }
                });

        // 生成随机数进行测试
        Map<String, Integer> resMap = new HashMap<>();

        List<String> plcList = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            String plchost = "192.168.0." + i + 1;
            for (int j = 0; j < 10; j++) {
                plcList.add(plchost + ":" + j + 100);
            }
        }

        for (int i = 0; i < plcList.size(); i++) {
            String plcwideget = plcList.get(i);
            String group = getServer(plcwideget);
            if (resMap.containsKey(group)) {
                resMap.put(group, resMap.get(group) + 1);
            } else {
                resMap.put(group, 1);
            }
        }

        resMap.forEach(
                (k, v) -> {
                    System.out.println("group " + k + ": " + v + "(" + v / 100.0D + "%)");
                });

        System.out.println("=========================================");

    }
}

可以看到 分布很均衡

5.Dubbo 一致性Hash 实现

最近给公司做的分发器 使用dubbo 调用远程服务,调研了一下 dubbo 也有自己实现的 一致性hash 不过实际使用起来 发现有些bug ,目前通过SPI机制 自己扩展了一下,来看看 dubbo的 一致性hash实现吧

5.1 版本 dubbo2.7.12

我用的版本是 dubbo2.7.12 已经入了 2.7的坑 不少坑都是自己慢慢调试解决的。

5.2 org.apache.dubbo.rpc.cluster.loadbalance

负载均衡基本就这些 一致性hash,随机 ,轮训,。。 没啥特别的

5.3 dubbo ConsistentHashLoadBalance源码

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.dubbo.rpc.cluster.loadbalance;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.support.RpcUtils;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import static org.apache.dubbo.common.constants.CommonConstants.COMMA_SPLIT_PATTERN;

/**
 * ConsistentHashLoadBalance
 */
public class ConsistentHashLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "consistenthash";

    /**
     * Hash nodes name
     */
    public static final String HASH_NODES = "hash.nodes";

    /**
     * Hash arguments name
     */
    public static final String HASH_ARGUMENTS = "hash.arguments";

    private final ConcurrentMap<String, ConsistentHashSelector<?>> selectors = new ConcurrentHashMap<String, ConsistentHashSelector<?>>();

    @SuppressWarnings("unchecked")
    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        String methodName = RpcUtils.getMethodName(invocation);
        String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
        // using the hashcode of list to compute the hash only pay attention to the elements in the list
        //有bug  1.invokers 老是变化,导致 不断的 在创建 ConsistentHashSelector 
				//      
        int invokersHashCode = invokers.hashCode();
        ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
        if (selector == null || selector.identityHashCode != invokersHashCode) {
            selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, invokersHashCode));
            selector = (ConsistentHashSelector<T>) selectors.get(key);
        }
        return selector.select(invocation);
    }

    private static final class ConsistentHashSelector<T> {

        private final TreeMap<Long, Invoker<T>> virtualInvokers;

        private final int replicaNumber;

        private final int identityHashCode;

        private final int[] argumentIndex;

        ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
            this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
            this.identityHashCode = identityHashCode;
            URL url = invokers.get(0).getUrl();
            //默认虚拟机节点数 160 ,可以通过
          /**
          dubbo:
            consumer:
    					parameters:
      					hash:
        					nodes: 560 #指定虚拟分片结点数  最终virtualInvokers = nodes * invokerCount
        					arguments: 0  指定的是 哪个参数作为 key
          	**/
            this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);
          //默认取 第 0 个参数 作为 hash的key
            String[] index = COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, HASH_ARGUMENTS, "0"));
            argumentIndex = new int[index.length];
            for (int i = 0; i < index.length; i++) {
                argumentIndex[i] = Integer.parseInt(index[i]);
            }
            for (Invoker<T> invoker : invokers) {
                String address = invoker.getUrl().getAddress();
                for (int i = 0; i < replicaNumber / 4; i++) {
                    byte[] digest = Bytes.getMD5(address + i);
                    for (int h = 0; h < 4; h++) {
                        long m = hash(digest, h);
                        virtualInvokers.put(m, invoker);
                    }
                }
            }
        }

        public Invoker<T> select(Invocation invocation) {
            String key = toKey(invocation.getArguments());
            byte[] digest = Bytes.getMD5(key);
            return selectForKey(hash(digest, 0));
        }

        private String toKey(Object[] args) {
            StringBuilder buf = new StringBuilder();
            for (int i : argumentIndex) {
                if (i >= 0 && i < args.length) {
                    buf.append(args[i]);
                }
            }
            return buf.toString();
        }

        private Invoker<T> selectForKey(long hash) {
            Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
            if (entry == null) {
                entry = virtualInvokers.firstEntry();
            }
            return entry.getValue();
        }

        private long hash(byte[] digest, int number) {
            return (((long) (digest[3 + number * 4] & 0xFF) << 24)
                    | ((long) (digest[2 + number * 4] & 0xFF) << 16)
                    | ((long) (digest[1 + number * 4] & 0xFF) << 8)
                    | (digest[number * 4] & 0xFF))
                    & 0xFFFFFFFFL;
        }
    }

}

6.Dubbo 的 一致性 hash的 bug 和 一些配置参数

6.1 invokers 不断的变化

经过调试 发现 invokers 时不时变化导致 一致在 rehash,其实 很多时候只是 节点的顺序变化而已

解决办法: 我直接把 invokers 节点数 取出来进行排序后拼接 成一个字符串 进行 计算hashcode 就不会总变化了

String invokeKey =
        invokers.stream()
                .filter(Node::isAvailable)
                // 按照ip 和port 排序
                .sorted(Comparator.comparing(invoke -> invoke.getUrl().getIp()))
                .sorted(Comparator.comparing(invoke -> invoke.getUrl().getPort()))
                .map(invoke -> invoke.getUrl().getIp() + ":" + invoke.getUrl().getPort())
                .collect(Collectors.joining(","));

6.2 注意 isAvailable

invokers中存在 节点不可用的,如果对于节点不可用的 直接过滤 需要注意 isAvailable

6.3 设置虚拟节点发片数

dubbo:
	consumer:
    parameters:
      hash:
        nodes: 560 #指定虚拟分片结点数  最终virtualInvokers = nodes * invokerCount ,默认是160

6.4 设置方法的哪个参数作为 hash的key

dubbo:
	consumer:
  	parameters:
    	hash:    	
      	arguments: 0  #默认就是0

7.SPI 扩展Dubbo 一致性hash 算法 ExtendConsistentHashLoadBalance

7.1 官方文档

官方文档很详细了

https://dubbo.apache.org/zh/docs/v3.0/references/spis/load-balance/

7.2 ExtendConsistentHashLoadBalance 扩展实现

package com.weareint.dispatchservice.extendconsistenthash;

import com.atw.mdc.entity.protocol.webRequest.PlcReadSimpleRequest;
import com.atw.mdc.entity.protocol.webRequest.PlcWriteSimpleRequest;
import com.weareint.dispatchservice.event.ConnectionRouteEvent;
import com.weareint.dispatchservice.event.NodeChangeEvent;
import com.weareint.dispatchservice.event.NodeChangeService;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.Node;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance;
import org.apache.dubbo.rpc.support.RpcUtils;
import org.springframework.context.ApplicationEventPublisher;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;

import static org.apache.dubbo.common.constants.CommonConstants.COMMA_SPLIT_PATTERN;

/**
 *
 *
 * <pre>
 * 扩展 一致性Hash 环  主要是把 hash 的key 只通过 plc 的deviceCode 进行hash , 然后后续添加 Redis 路由表 进行
 * </pre>
 *
 * @author johnny
 * @date 2021-08-26 5:32 下午
 */
@Slf4j
public class ExtendConsistentHashLoadBalance extends AbstractLoadBalance {

    public static ApplicationEventPublisher publisher;
    public static NodeChangeService nodeChangeService;

    public static final String NAME = "consistenthash";

    /** Hash nodes name */
    public static final String HASH_NODES = "hash.nodes";

    /** Hash arguments name */
    public static final String HASH_ARGUMENTS = "hash.arguments";

    private final ConcurrentMap<String, ExtendConsistentHashLoadBalance.ConsistentHashSelector<?>>
            selectors =
                    new ConcurrentHashMap<
                            String, ExtendConsistentHashLoadBalance.ConsistentHashSelector<?>>();

    public ConcurrentMap<String, ConsistentHashSelector<?>> getSelectors() {
        return selectors;
    }

    @SuppressWarnings("unchecked")
    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        String methodName = RpcUtils.getMethodName(invocation);
        // String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
        // 只要是 servicekey 就好
        String key = invokers.get(0).getUrl().getServiceKey();
        // String key = invokers.get(0).getUrl().getParameter("remote.application");
        if (log.isDebugEnabled()) {
            log.info("【remoteApplication:{}】", key);
        }
        // using the hashcode of list to compute the hash only pay attention to the elements in the
        String invokeKey =
                invokers.stream()
                        .filter(Node::isAvailable)
                        // 按照ip 和port 排序
                        .sorted(Comparator.comparing(invoke -> invoke.getUrl().getIp()))
                        .sorted(Comparator.comparing(invoke -> invoke.getUrl().getPort()))
                        .map(invoke -> invoke.getUrl().getIp() + ":" + invoke.getUrl().getPort())
                        .collect(Collectors.joining(","));

        if (log.isDebugEnabled()) {
            log.info("【invokeKey : {}】", invokeKey);
        }
        int invokersHashCode = invokeKey.hashCode();
        ExtendConsistentHashLoadBalance.ConsistentHashSelector<T> selector = null;
        // 同时submit 可能会有问题 加个锁 可能会有第一次提交生成 虚拟节点     
        selector = (ExtendConsistentHashLoadBalance.ConsistentHashSelector<T>) selectors.get(key);
        // 判断是否invokers 有变化  selector.identityHashCode != invokersHashCode
        if (selector == null || selector.identityHashCode != invokersHashCode) {
            synchronized (ExtendConsistentHashLoadBalance.class) {
                selector =
                        (ExtendConsistentHashLoadBalance.ConsistentHashSelector<T>)
                                selectors.get(key);
                if (selector == null || selector.identityHashCode != invokersHashCode) {
                    // 这个 isAvailable 要存在 否则有bug
                    List<Invoker<T>> availableInvoker =
                            invokers.stream()
                                    .filter(Node::isAvailable)
                                    .collect(Collectors.toList());
                    ConsistentHashSelector<T> tConsistentHashSelector =
                            new ConsistentHashSelector<>(
                                    availableInvoker, methodName, invokersHashCode);
                    selectors.put(key, tConsistentHashSelector);
                    selector =
                            (ExtendConsistentHashLoadBalance.ConsistentHashSelector<T>)
                                    selectors.get(key);
                    log.info(
                            "【new selector by invokeKey : {} , availableInvoker:{}】",
                            invokeKey,
                            availableInvoker.stream()
                                    .map(
                                            invoke ->
                                                    invoke.getUrl().getIp()
                                                            + ":"
                                                            + invoke.getUrl().getPort())
                                    .collect(Collectors.joining(",")));

                    NodeChangeEvent event = new NodeChangeEvent(this, tConsistentHashSelector);
                    publisher.publishEvent(event);
                }
            }
        }
        String hashKey = selector.toKey(invocation.getArguments());
        Invoker<T> select = selector.select(hashKey);
        log.info(
                "【plcDeviceCode: {} dispatch to  ip : {}:{}",
                hashKey,
                select.getUrl().getIp(),
                select.getUrl().getPort());
        // deviceCode route table  To Redis
        ConnectionRouteEvent event = new ConnectionRouteEvent(this, hashKey, select, selector);
        publisher.publishEvent(event);
        return select;
    }

    public static final class ConsistentHashSelector<T> {

        private final TreeMap<Long, Invoker<T>> virtualInvokers;

        public final List<Invoker<T>> invokers;

        private final int replicaNumber;

        private final int identityHashCode;

        private final int[] argumentIndex;

        ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
            this.invokers = invokers;
            this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
            this.identityHashCode = identityHashCode;
            URL url = invokers.get(0).getUrl();
            this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);
            String[] index =
                    COMMA_SPLIT_PATTERN.split(
                            url.getMethodParameter(methodName, HASH_ARGUMENTS, "0"));
            argumentIndex = new int[index.length];
            for (int i = 0; i < index.length; i++) {
                argumentIndex[i] = Integer.parseInt(index[i]);
            }
            for (Invoker<T> invoker : invokers) {
                String address = invoker.getUrl().getAddress();
                for (int i = 0; i < replicaNumber / 4; i++) {
                    byte[] digest = md5(address + i);
                    for (int h = 0; h < 4; h++) {
                        long m = hash(digest, h);
                        virtualInvokers.put(m, invoker);
                    }
                }
            }
        }

        public Invoker<T> select(String key) {
            byte[] digest = md5(key);
            return selectForKey(hash(digest, 0));
        }

        @SuppressWarnings("unchecked")
        public String toKey(Object[] args) {
            StringBuilder buf = new StringBuilder();
            for (int i : argumentIndex) {
                if (i >= 0 && i < args.length) {
                    // 只取 PlcReadSimpleRequest的 DeviceCode 作为 hash的key
                    if (args[i] instanceof ArrayList) {
                        ArrayList<PlcReadSimpleRequest> list =
                                (ArrayList<PlcReadSimpleRequest>) args[i];
                        buf.append(list.get(0).getDeviceCode());
                        // 只取 PlcWriteSimpleRequest DeviceCode 作为 hash的key
                    } else if (args[i] instanceof PlcWriteSimpleRequest) {
                        PlcWriteSimpleRequest req = (PlcWriteSimpleRequest) args[i];
                        buf.append(req.getDeviceCode());
                    } else if (args[i] instanceof String) {
                        // PlcConnectionRequest req = (PlcConnectionRequest) args[i];
                        // 关闭连接
                        String deviceCode = (String) args[i];
                        buf.append(deviceCode);
                    } else {
                        buf.append(args[i]);
                    }
                }
            }
            return buf.toString();
        }

        private Invoker<T> selectForKey(long hash) {
            Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
            if (entry == null) {
                entry = virtualInvokers.firstEntry();
            }
            return entry.getValue();
        }

        private long hash(byte[] digest, int number) {
            return (((long) (digest[3 + number * 4] & 0xFF) << 24)
                            | ((long) (digest[2 + number * 4] & 0xFF) << 16)
                            | ((long) (digest[1 + number * 4] & 0xFF) << 8)
                            | (digest[number * 4] & 0xFF))
                    & 0xFFFFFFFFL;
        }

        private byte[] md5(String value) {
            MessageDigest md5;
            try {
                md5 = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            md5.reset();
            byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
            md5.update(bytes);
            return md5.digest();
        }

        public int getIdentityHashCode() {
            return identityHashCode;
        }
    }
}

7.3 配置spi扩展

extendconsistenthash=com.weareint.dispatchservice.extendconsistenthash.ExtendConsistentHashLoadBalance

7.4 使用自定义的扩展 loadbalance

@DubboReference(loadbalance = "extendconsistenthash")
private IDeviceWriteService deviceWriteService;

7.5 已知扩展

总结

本篇主要讲解了 什么是一致性 hash 它有哪些优点和存在的问题,以及 带虚拟节点的一致性hash,最后介绍了一些 dubbo 的一致性hash 实现,dubbo自带的有bug ,但是提供了 spi 扩展机制 你可以自己去实现 ,目前我就是这样去的

「 分布式技术 」一致性哈希算法(Hash)详解 在分布式系统中,数据的存储和访问是很重要的问题。为了提高系统的可用性和扩展性,常常需要将数据分布到不同的节点上,而且这些节点也可能会动态地加入或离开集群。一致性哈希算法就是一种常用的解决方案,它可以解决节点的动态变化和负载均衡的问题。本文将深入探讨一致性哈希算法的底层原理,包括其基本思想、关键步骤以及优缺点等,同时结合实际场景进行举例说明。一致性哈希算法是一种用于分布式系统中的数据分片和负载均衡的算法。它将整个哈希空间划分为一个环,并且每个节点在这个环上都有一个对应的位置。 阅读详情

相关推荐

一篇文章搞懂C++实现哈希算法

哈希算法,也称为散列算法,是一种从任意长度的输入数据创建固定大小输出的方法,这种输出通常被称为“哈希值”、“散列值”或简单地“哈希”。在计算机科学中,哈希算法主要用于快速数据查找和数据结构中的高效数据管理,如哈希表。哈希也是加密和数据完整性验证的基础。哈希算法通过一个称为哈希函数的数学过程来运作。这个函数接受输入(称为“预映射值”)并返回一个通常较短、固定长度的哈希值。这个哈希值在理想情况下为每个不同的输入值提供一个唯一的标识(尽管在实际中这并不总是可能的)。

weixin_73570479的博客 4928

一致性 hash 环

一致性 hash 环 最近做项目 做了一个分发器 ,需要 根据请求携带的参数 把请求分发到 不同的服务器上面,最终我选择使用 一致性hash 环 来实现 ,本篇 就主要讲解一下 一致性hash环 它的基本原理 概述 一致性hash算法 由于 均衡性 持久性的映射特点 被广泛应用于负载均衡领域,比如 nginx 、dubbo 、等等 内部都有一致性hash 的实现 ,比如 dubbo ,当你调用rpc 接口的时候,如果有2个提供者,那么你可以通过配置 让其调用通过 一致性hash 进行计算 然后

qq_34285557的博客 1246

【数据结构与算法】哈希算法的原理和应用详解!

在程序员的实际开发中,哈希算法常常能用得到,本文以哈希算法的原理和应用为核心,和大家详细讲解一下哈希算法的概念、常见算法以及原理、在信息安全的应用等等。 一、概念 哈希表就是一种以 键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值。哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键..

致力于C语言C++知识分享! 2319

哈希算法:完整讲解与实现

哈希算法(Hash Algorithm)是一种映射函数,将任意长度的输入数据映射为固定长度的输出数据。这个输出称为哈希值(Hash Value)或消息摘要特点:将输入映射为固定的 128 位哈希值。快速,但安全性较低。用途:文件完整性校验。SHA-1:生成 160 位哈希值,比 MD5 更安全,但已被淘汰。SHA-256:更安全的算法,生成 256 位哈希值,广泛用于密码学。特点:快速计算,生成 32 位校验值,主要用于数据传输。用途:网络协议中的数据完整性验证。

Interview_TC的博客 1万+

基于STM32的家用新风系统智能控制器 | 保姆式教程 开源可复刻

本项目基于STM32F103C8T6核心控制器,完整实现了家用新风系统智能控制器的全功能开发,覆盖多传感器数据采集、双模式控制、PID调速、异常报警、掉电记忆等核心功能,全程采用保姆式无跳步教学,从硬件选型、接线、CubeMX配置、代码开发、调试排坑全流程拆解,跟着步骤一步步操作,就能顺利完成项目复刻

ZhongWeiXiang9的博客 470

二叉树的操作之统计二叉树中节点的个数

一,问题描述 给定一颗二叉树,已知其根结点。 ①计算二叉树所有结点的个数 ②计算二叉树中叶子结点的个数 ③计算二叉树中满节点(度为2)的个数   二,算法分析 找出各个问题的基准条件,然后采用递归的方式实现。 ①计算二叉树所有结点的个数 1)当树为空时,结点个数为0,否则为根节点个数 加上 根的左子树中节点个数 再加上 根的右子树中节点的个数 借助遍历二叉树的思路,每访问一个结...

weixin_34200628的博客 4568

一致性哈希算法原理详解

(1)一致性哈希算法将整个哈希值空间按照顺时针方向组织成一个虚拟的圆环,称为 Hash 环; (2)接着将各个服务器使用 Hash 函数进行哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,从而确定每台机器在哈希环上的位置; (3)最后使用算法定位数据访问到相应服务器:将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针寻找,第一台遇到的服务器就是其应该定位到的服务器

张维鹏的博客 5万+

案例分析|一篇文章带你看懂养鸡场智慧管理系统

养鸡场智慧管理系统是为了满足现代化养鸡业对精细化管理的需求而设计的一款专业软件。随着科技的发展和农业产业的升级,传统的手工记录和管理方式已无法满足高效、准确和实时的数据跟踪需求。本软件旨在通过集成各种功能模块,帮助养鸡场主实现从鸡苗孵化、饲养管理、饲料控制到销售结算等全链条的智能化操作。该系统的核心目标是提升养鸡场的运营效率和盈利能力,通过自动化数据处理,减少人工误差,节省时间和精力。例如,孵化报表和记录模块可以帮助管理者实时监控孵化过程,及时调整策略;

muyu2980的专栏 930

哈希算法

对于哈希算法,在我们平时的开发中,都是基本上拿来就用就行了,所以这节我们将重点放在如何使用,并不进行哈希算法的原理剖析和如何设计一个哈希算法的讲解. 什么是哈希算法? 将任意长度的二进制值串映射为固定长度的二进制值串,这个映射的规则就是哈希算法,而通过原始数据映射之后得到的二进制值串就是哈希值(散列值).一个优秀的哈希算法需要满足: 从哈希值不能反向推导出原始数据(所以哈希算法也叫单向哈希算法)...

Tattoo的博客 4万+

Toeplitz矩阵以及矩阵乘法FFT加速

Toeplitz矩阵以及矩阵乘法FFT加速 1.Toeplitz矩阵 托普利兹矩阵,简称为T型矩阵,它是由Bryc、Dembo、Jiang于2006年提出的。托普利兹矩阵的主对角线上的元素相等,平行于主对角线的线上的元素也相等;矩阵中的各元素关于次对角线对称,即T型矩阵为次对称矩阵。这里我们使用matlab中自带的函数生成一个toeplitz矩阵的例子: x=[1 2 3 4]; y=[1 5 6 7 8 9]; z=toeplitz(x,y); 我们可以得到一个这样的矩阵结果: 我们可以看到,其中x中

thlz 5069

使用python中pypower进行电力潮流计算

使用python中pypower进行电力潮流计算

总裁余(余登武)博客 4863

最通俗易懂的一致性哈希算法原理

再讨论一致性哈希之前,我们先来回顾一下缓存的演化历史: 当我们的系统还是一个非常小的时候,对于用户的请求我们直接访问数据库就好了。当访问量到了一定数量的时候,我们使用缓存服务器,缓存一部分热数据来减轻数据库的压力,这便是最原始的阶段。

不爱学习 2936

通俗易懂的哈希算法讲解

哈希是一种加密算法 哈希函数(Hash Function),也称为散列函数或杂凑函数。哈希函数是一个公开函数,可以将任意长度的消息M映射成为一个长度较短且长度固定的值H(M),称H(M)为哈希值、散列值(Hash Value)、杂凑值或者消息摘要(Message Digest)。它是一种单向密码体制,即一个从明文到密文的不可逆映射,只有加密过程,没有解密过程。 它的函数表达式为:h=H(m) ...

zongyue_wang的博客 4万+

华为ONU远程登录管理配置

华为P602E、EG8040CONU、EG8240H5、W626E等ONU、ONT默认无法配置管理IP,无法远程telnet登录管理。校园网项目中因故障诊断需要远程登录ONU可以通过OLT的远程WEB代理功能链接登录,具体配置如下:

symstandsun的博客 6112

高通平台蓝牙学习-- 基于WCN6750 Bluetooth 5.2的讲解

摘要:蓝牙5.2解决方案WCN6750具备多项先进特性:完全兼容5.2标准及ANT+协议,支持2Mbps高速BLE和长距离传输;采用射频集成方案,与WLAN共享天线并支持5G并发;内置功率放大器,支持Class1/2传输;提供双音频流和Slimbus接口优化。其HCI UART接口支持3.2Mbps速率,通过BlueZ协议栈实现蓝牙5.2功能,包括2Mbps模式和长距离传输,符合核心规范要求。(149字)

专业开发者的博客,一个从事Android WiFi和蓝牙的工程师 3万+

2024年Go最全一致性哈希算法原理详解,2024最新大厂Golang面试真题解析

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新如果你需要这些资料,可以戳这里获取上述HASH算法时,会出现一些缺陷:如果服务器已经不能满足缓存需求,就需要增加服务器数量,假设我们增加了一台缓存服务器,此时如果仍然使用上述方法对同一张图片进行缓存,那么这张图片所在的服务器编号必定与原来3台服务器时所

2401_84911346的博客 1199

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

哈希算法讲解

哈希是密码学的基础,理解哈希是理解数字签名和加密通信等技术的必要前提。 一. 基本含义: 哈希,英文是 hash ,本来意思是”切碎并搅拌“,有一种食物就叫 Hash ,就是把食材切碎并搅拌一下做成的。哈希函数的运算结果就是哈希值,通常简称为哈希。哈希函数有时候也翻译做散列函数。 根据维基百科的定义,哈希函数要做的事情是给一个任意大小的数据生成出一个固定长度的数据,作为它的映射。所谓映射就是......

EmotionComputer 2296

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

字符串哈希算法

哈希算法 字符串的哈希算法,通俗的理解,就是将一个字符串,转化成整数 原来我们进行字符串匹配的时候,就是一个个去匹配,那么时间复杂度是o(n),如果转化成数字,去匹配那么时间复杂度会变成o(1)。 哈希算法的引入 首先联想一下二进制数,对于任意一个二进制数,我们将它化为10进制的数的方法如下(以二进制数1101101为例): 1101101=1 * 2 ^ 6+1 * 2 ^ 5+0 * 2 ^ ...

a921893396的博客 5737

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

Hash 浅谈哈希算法

hash 哈希算法

young_1217的博客 1378
上一篇: Netty之DefaultAttributeMap与AttributeKey的机制和原理
下一篇: Redis+Caffeine两级缓存,让访问速度纵享丝滑
JavaMonsterr
博客等级 码龄4年 211粉丝 481原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值