一致性hash算法讲解——Java实现

一致性hash算法讲解

hash

hash(哈希):即是将一个大的集合中的元素映射到一个特定大小的集合中的操作。

普通hash的弊端

普通的hash一般采用求余的方式得到映射关系,在java中hashmap中的hash索引的策略就是hashcode % table.size()-->实际为 : hashcode & (table.size() - 1),但是这个是在节点的数量一定的情况下,在redis中的,redis主要是做缓存用的,如果一个redis集群需要增加或者删除一个节点,那么采用这种普通的hash策略会造成大量的key不能够命中,进而产生缓存失效,大量的请求到达数据库,如果数据库没有做高可用处理,那么在大量的请求到数据库时,很容易就将数据库服务打崩。

举个例子:
一个含有3个节点的redis集群,现在需要增加一个新的redis节点,在这种情况下,会造成75%的缓存失效,进而会有75%的请求直接打到数据库,随着集群节点的数量不断的增大,在原有的集群的基础之上,添加一个新的节点,会造成更大比例的缓存失效。
原始集群节点数量 m ,失效缓存比例:m / (m + 1);

一致性hash原理

一致性hash是将映射集合规定为一个0~2^32-1的范围的环,收尾相连。

node和key分别按照hash函数计算,最后得到一个值,在环上会有一个位置与之对应,如图,有三个node和三个key在环上,每一个key从当前位置出发顺时针寻找离它最近的一个node,作为最终的node,环上的其他key节点按照同样的方式寻找与之对应的node。

但是这里还有一个问题,就是每个node的位置不一定是均匀地分布在环上的,如果不均匀,那么就会有某个节点承担的请求缓存压力相比较于其他节点要多,所以为了均衡各个节点的压力,node节点应当尽量均匀地分布在环上,为此,引入一个虚拟节点的概率,即是,每一个物理节点会有多个与之对应的虚拟节点, 然后让虚拟节点在环上均匀的分布,如此来达到物理节点巧妙地分布在环上,解决物理节点在环上分布不均匀的问题。

代码实现

public class ConsistentHash {

    private int virNodesNum;

    private Map<String, List<Integer>> realNodeToVirNodesHashCode = new ConcurrentHashMap<>();

    private SortedMap<Integer, String> ring = new TreeMap<>();


    public ConsistentHash(int virNodesNum) {
        this.virNodesNum = virNodesNum;
    }


    public synchronized void addNode(String node) {
        if (realNodeToVirNodesHashCode.containsKey(node))
            throw new RuntimeException(node + " has already in the pool.");
        // 创建虚拟节点,并放置到环上去
        // 虚拟节点的hashcode 放入到 realNodeToVirNodesHashCode 中
        int hash;
        List<Integer> hashCodes = new ArrayList<>();
        for (int i = 0; i < this.virNodesNum; i++) {
            hash = rehash(node + "---" + i);
            ring.putIfAbsent(hash, node);
            hashCodes.add(hash);
        }
        realNodeToVirNodesHashCode.put(node, hashCodes);
    }

    public synchronized void removeNode(String node) {
        if (!realNodeToVirNodesHashCode.containsKey(node))
            throw new RuntimeException(node + " is not in the pool.");
        // 得到该节点对应的虚拟节点hashcode列表
        List<Integer> virNodes = realNodeToVirNodesHashCode.get(node);
        // 从环上依个删除
        for (Integer virNode : virNodes) {
            ring.remove(virNode);
        }
        realNodeToVirNodesHashCode.remove(node);
    }

    public String getNode(String key) {
        int hash = rehash(key);
        // 从环上选择一个顺时针方向
        // (升序方向--或者降序方向都可,这里升序)的
        // 第一个虚拟节点作为目标节点返回
        SortedMap<Integer, String> tailMap = ring.tailMap(hash); // tailMap中的所有key值>=hash
        return tailMap.isEmpty() ? ring.get(ring.firstKey()) : tailMap.get(tailMap.firstKey());
    }


    private int rehash(Object o) {
        int hash = o.hashCode();
        hash *= 16777619;// 32 bit FNV_prime = 2^24 + 2^8 + 0x93 = 16777619
        return Math.abs(hash ^ (hash >>> 16));
    }

    public static void main(String[] args) {
        ConsistentHash consistentHash = new ConsistentHash(150);
        consistentHash.addNode("192.168.10.10");
        consistentHash.addNode("192.168.10.11");
        consistentHash.addNode("192.168.10.12");
        consistentHash.addNode("192.168.10.13");
        String [] tmp = new String[1000000];
        for (int i = 0; i < 1000000; i++) {
            tmp[i] = consistentHash.getNode(String.valueOf(i));
        }
        consistentHash.addNode("192.168.10.14");
        int cnt = 0;
        for (int i = 0; i < 1000000; i++) {
            if (tmp[i].equals(consistentHash.getNode(String.valueOf(i)))) cnt++;
            tmp[i] = consistentHash.getNode(String.valueOf(i));
        }
        System.out.println("添加一个节点后命中率:" + cnt *1.0 / 1000000 * 100 + "%");
        consistentHash.removeNode("192.168.10.12");
        cnt = 0;
        for (int i = 0; i < 1000000; i++) {
            if (tmp[i].equals(consistentHash.getNode(String.valueOf(i)))) cnt++;
        }
        System.out.println("移除一个节点后命中率:" + cnt *1.0 / 1000000 * 100 + "%");
    }


}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值