一致性hash算法

1为啥会有这种算法的诞生,它解决什么问题?
在说这个算法之前,我们看看以前做缓存(一致性hash算法不仅仅局限在做缓存上)的思路。
假如现在我们有一个网站,redis上数据已经超过了单机的承受能力,现在就要对其做进行分库分表,则分库分表就意味着要有一个路由规则,假设我们的路由规则是hash取模的方式,根据这个路由规则我们就弄一个redis集群,假设集群中有四个节点,节点A,节点B,节点C,节点D,假如一个用户访问redis集群的某一张图片,我们假定该图片叫做a.png,如下图所示:


由于有4台服务器,假设hash(a.png)计算的值为8(一般我们取a.png的名称作为值进行hash), 因此公式hash(a.png) % 4 = 2 ,可知定位到了节点B服务器,这样该用户就获取都a.png的图片了,过了一会以后,运维还是感觉到集群中的每个节点负载都比较高,然后给集群中加入一个节点,假设该节点叫做节点F,当该用户再次访问a.png的图片,通过路由规则就获取不到图片了,
因为hash(a.png) % 4已经变为了hash(a.png) % 5,造成了redis集群上的所有缓存全部失效。为了解决这个问题,现在就引入了一致性hash算法来解决这个问题。

2一致性hash算法
为了能直观的理解一致性hash原理,这里结合一个简单的例子来讲解,假设有4台服务器组成redis集群,各个集群节点的名称分别为NodeA,NodeB,NodeC,NodeD。
第一步:一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环,这个圆环就叫做hash环。该圆环长度为2^32,它值空间为0-2^32-1(即哈希值是一个32位无符号整形)。如图所示:
 

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


第三步:当有数据存入到redis集群时,将数据key使用前面计算服务器的hash函数计算出数据key的哈希值,并把计算出来的值映射到hash环上(映射关系为计算出来的值就是hash环上的整数值),假定有一股风从环上的0点顺时针吹,一个数据作为一个小球,一个服务器节点作为小箱子,那么这股风一吹,每个数据都会进入到最近顺时针的服务器节点上,也就是服务器节点存储该数据了。例如我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:

根据一致性Hash算法,数据A会被定为到Node A上,B被定为到Node B上,C被定为到Node C上,D被定为到Node D上。
3一致性Hash算法的容错性和可扩展性
现假设Node C宕机了,根据一致性hash算法可知,此时对象A、B、D不会受到影响,只有C对象被重定位到Node D,如下所示:

我们可以看到该算法解决了缓存大量失效的问题,假如集群中某一个节点增加或者删除都不会对集群中全部的缓存造成影响,只会造成顺时针上该节点的下一个节点造成影响。

4虚拟节点
通过上面的表述中我们可以看到,Node C宕机了,它所有的数据都是要到Node D上去获取,原本Node D上就有缓存,现在又要把Node C的请求压力给Node D,那么Node D负载就上来了,那么怎样解决这个问题呢?答案是引入虚拟节点,下面我们来聊聊虚拟节点是怎样实现集群间负载均衡的。
假设集群中只有两台服务器,其环分布如下:

此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上。为了解决这种数据倾斜问题,一致性Hash算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器IP或主机名的后面增加编号来实现。

例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 Node A#1,Node A#2,Node A#3,Node B#1,Node B#2,Node B#3的哈希值,于是形成六个虚拟节点,如图所示:

同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到Node A#1,Node A#2,Node A#3三个虚拟节点的数据均定位到Node A上。这样就解决了某一个节点负载过高的问题。
下面给出一致性hash算法的实现:
 

package com.example.demo.arithmetic;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Map;
import java.util.TreeMap;

/**
 * Created by markcd on 2018/2/28.
 */
public class ConsistentHashLoadBalanceNoVirtualNode {

    private TreeMap<Long, String> realNodes = new TreeMap<>();
    private String[] nodes;

    public ConsistentHashLoadBalanceNoVirtualNode(String[] nodes){
        this.nodes = Arrays.copyOf(nodes, nodes.length);
        initalization();
    }

    /**
     * 初始化哈希环
     * 循环计算每个node名称的哈希值,将其放入treeMap
     */
    private void initalization(){
        for (String nodeName: nodes) {
            realNodes.put(hash(nodeName, 0), nodeName);
        }
    }

    /**
     * 根据资源key选择返回相应的节点名称
     * @param key
     * @return 节点名称
     */
    public String selectNode(String key){
        Long hashOfKey = hash(key, 0);
        if (! realNodes.containsKey(hashOfKey)) {
               //ceilingEntry()的作用是得到比hashOfKey大的第一个Entry
            Map.Entry<Long, String> entry = realNodes.ceilingEntry(hashOfKey);
            if (entry != null)
                return entry.getValue();
            else
                return nodes[0];
        }else
            return realNodes.get(hashOfKey);
    }

    private Long hash(String nodeName, int number) {
        byte[] digest = md5(nodeName);
        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;
    }

    /**
     * md5加密
     *
     * @param str
     * @return
     */
    public byte[] md5(String str) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.reset();
            md.update(str.getBytes("UTF-8"));
            return md.digest();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }

    private void printTreeNode(){
        if (realNodes != null && ! realNodes.isEmpty()){
            realNodes.forEach((hashKey, node) ->
                    System.out.println(
                            new StringBuffer(node)
                            .append(" ==> ")
                            .append(hashKey)
                    )
            );
        }else
            System.out.println("Cycle is Empty");
    }

    public static void main(String[] args){
        String[] nodes = new String[]{"192.168.2.1:8080", "192.168.2.2:8080", "192.168.2.3:8080", "192.168.2.4:8080"};
        ConsistentHashLoadBalanceNoVirtualNode consistentHash = new ConsistentHashLoadBalanceNoVirtualNode(nodes);
        consistentHash.printTreeNode();
    }
}

一致性hash算法优化(加入虚拟节点)的实现:

package com.example.demo.arithmetic;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedList;
import java.util.Map;
import java.util.TreeMap;

/**
 * Created by markcd on 2018/2/28.
 */
public class ConsistentHashLoadBalance {

    private TreeMap<Long, String> virtualNodes = new TreeMap<>();
    private LinkedList<String> nodes;
        //每个真实节点对应的虚拟节点数
    private final int replicCnt;

    public ConsistentHashLoadBalance(LinkedList<String> nodes, int replicCnt){
        this.nodes = nodes;
        this.replicCnt = replicCnt;
        initalization();
    }

    /**
     * 初始化哈希环
     * 循环计算每个node名称的哈希值,将其放入treeMap
     */
    private void initalization(){
        for (String nodeName: nodes) {
            for (int i = 0; i < replicCnt/4; i++) {
                String virtualNodeName = getNodeNameByIndex(nodeName, i);
                for (int j = 0; j < 4; j++) {
                    virtualNodes.put(hash(virtualNodeName, j), nodeName);
                }
            }
        }
    }

    private String getNodeNameByIndex(String nodeName, int index){
        return new StringBuffer(nodeName)
                .append("&&")
                .append(index)
                .toString();
    }

    /**
     * 根据资源key选择返回相应的节点名称
     * @param key
     * @return 节点名称
     */
    public String selectNode(String key){
        Long hashOfKey = hash(key, 0);
        if (! virtualNodes.containsKey(hashOfKey)) {
            Map.Entry<Long, String> entry = virtualNodes.ceilingEntry(hashOfKey);
            if (entry != null)
                return entry.getValue();
            else
                return nodes.getFirst();
        }else
            return virtualNodes.get(hashOfKey);
    }

    private Long hash(String nodeName, int number) {
        byte[] digest = md5(nodeName);
        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;
    }

    /**
     * md5加密
     *
     * @param str
     * @return
     */
    public byte[] md5(String str) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.reset();
            md.update(str.getBytes("UTF-8"));
            return md.digest();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }

    public void addNode(String node){
        nodes.add(node);
        String virtualNodeName = getNodeNameByIndex(node, 0);
        for (int i = 0; i < replicCnt/4; i++) {
            for (int j = 0; j < 4; j++) {
                virtualNodes.put(hash(virtualNodeName, j), node);
            }
        }
    }

    public void removeNode(String node){
        nodes.remove(node);
        String virtualNodeName = getNodeNameByIndex(node, 0);
        for (int i = 0; i < replicCnt/4; i++) {
            for (int j = 0; j < 4; j++) {
                virtualNodes.remove(hash(virtualNodeName, j), node);
            }
        }
    }

    private void printTreeNode(){
        if (virtualNodes != null && ! virtualNodes.isEmpty()){
            virtualNodes.forEach((hashKey, node) ->
                    System.out.println(
                            new StringBuffer(node)
                                    .append(" ==> ")
                                    .append(hashKey)
                    )
            );
        }else
            System.out.println("Cycle is Empty");
    }

    public static void main(String[] args){
        LinkedList<String> nodes = new LinkedList<>();
        nodes.add("192.168.2.1:8080");
        nodes.add("192.168.2.2:8080");
        nodes.add("192.168.2.3:8080");
        nodes.add("192.168.2.4:8080");
        ConsistentHashLoadBalance consistentHash = new ConsistentHashLoadBalance(nodes, 160);
        consistentHash.printTreeNode();
    }
}

转载的博客地址:

https://blog.csdn.net/bntX2jSQfEHy7/article/details/79549368

https://www.cnblogs.com/markcd/p/8476237.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值