一致性哈希算法

11 篇文章 1 订阅
2 篇文章 0 订阅

参考:
什么是一致性Hash算法
几种经典的hash算法
一致性哈希算法
一致性哈希算法学习及JAVA代码实现分析

原理

一致性Hash算法的原理
因为对于hash(k)的范围在int范围,所以我们将0~2^32作为一个环。其步骤为:
1,求出每个服务器的hash(服务器ip)值,将其配置到一个 0~2^n 的圆环上(n通常取32)。
2,用同样的方法求出待存储对象的主键 hash值,也将其配置到这个圆环上,然后从数据映射到的位置开始顺时针查找,将数据分布到找到的第一个服务器节点上。
其分布如图:
这里写图片描述


Java实现

import java.util.ArrayList;
import java.util.Random;
import java.util.SortedMap;
import java.util.TreeMap;


/**
 * 服务器节点类
 */
class ServerNode {
    //节点ip
    private String ip;
    //节点其他属性.....

    public ServerNode(String ip) {
        this.ip = ip;
    }

    /**
     * 这里简单的重写了一下hashCode方法,利用FNV1_32_HASH算法
     * 添加其他属性时,要及时更新该方法
     */
    @Override
    public int hashCode() {
        final int p = 16777619;
        int hash = (int) 2166136261L;
        for (int i = 0; i < ip.length(); i++)
            hash = (hash ^ ip.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;
    }


    public String getIp() {
        return ip;
    }

    public void setIp(String ip) {
        this.ip = ip;
    }
}

/**
 * 任务节点
 */
class TaskNode {
    //任务id
    private String id;

    //任务其他属性
    public TaskNode(String id) {
        this.id = id;
    }

    /**
     * 这里简单的重写了一下hashCode方法,利用FNV1_32_HASH算法
     * 添加其他属性时,要及时更新该方法
     */
    @Override
    public int hashCode() {
        final int p = 16777619;
        int hash = (int) 2166136261L;
        for (int i = 0; i < id.length(); i++)
            hash = (hash ^ id.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;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}


/**
 * 不带虚拟节点的一致性Hash算法
 *
 * @author zf
 */
public class ConsistentHashing {


    //key表示服务器的hash值,value表示服务器
    private SortedMap<Integer, ServerNode> sortedMap = new TreeMap<>();

    /**
     * 初始化
     *
     * @param serverNodeArrayList 服务器节点列表
     */
    public ConsistentHashing(ArrayList<ServerNode> serverNodeArrayList) {
        for (ServerNode node : serverNodeArrayList) {
            sortedMap.put(node.hashCode(), node);
        }
    }

    /**
     * 获取该任务节点分配到的服务器节点
     *
     * @return
     */
    public ServerNode getServerNode(TaskNode taskNode) {
        int taskHash = taskNode.hashCode();
        // 得到大于该Hash值的所有Map
        SortedMap<Integer, ServerNode> subMap = sortedMap.tailMap(taskHash);
        //为空时,即访问到最后一个节点,其顺时针下一个节点为一号节点
        if (subMap.size() == 0) {
            return sortedMap.get(sortedMap.firstKey());
        }
        // 第一个Key就是顺时针过去离node最近的那个结点
        Integer i = subMap.firstKey();

        // 返回对应的服务器节点
        return subMap.get(i);
    }


    /**
     * 向集群内添加新的服务器节点(待实现)
     *
     * @return
     */
    public boolean addServerNode(ServerNode serverNode) {
        return true;
    }

    /**
     * 从集群中移除一个服务器节点(待实现)
     *
     * @param serverNode 服务器节点
     * @return
     */
    public boolean removeServerNode(ServerNode serverNode) {
        return true;
    }

    public static void main(String[] args) {
        //待添加入Hash环的服务器列表ip
        String[] serverIp = {"192.168.0.0:111", "192.168.0.1:111",
                "192.168.0.2:111", "192.168.0.3:111", "192.168.0.4:111"};
        ArrayList<ServerNode> serverNodeArrayList = new ArrayList<>();
        for (String ip : serverIp) {
            serverNodeArrayList.add(new ServerNode(ip));
        }
        ConsistentHashing consistentHashing = new ConsistentHashing(serverNodeArrayList);
        for (int i = 0; i < 100; i++) {
            System.out.println(i + "节点分配的服务器ip为:" + consistentHashing.getServerNode(new TaskNode("" + new Random().nextInt())).getIp());

        }

    }
}

和一般的取模,分段方法相比一致性hash的优势

传统的取模方式

例如10条数据,3个节点,如果按照取模的方式,那就是

node a: 0,3,6,9

node b: 1,4,7

node c: 2,5,8

当增加一个节点的时候,数据分布就变更为

node a:0,4,8

node b:1,5,9

node c: 2,6

node d: 3,7

总结:数据3,4,5,6,7,8,9在增加节点的时候,都需要做搬迁,成本太高

一致性哈希方式

最关键的区别就是,对节点和数据,都做一次哈希运算,然后比较节点和数据的哈希值,数据取和节点最相近的节点做为存放节点。这样就保证当节点增加或者减少的时候,影响的数据最少。

还是拿刚刚的例子,(用简单的字符串的ascii码做哈希key):

十条数据,算出各自的哈希值

0:192

1:196

2:200

3:204

4:208

5:212

6:216

7:220

8:224

9:228

有三个节点,算出各自的哈希值

node a: 203

node g: 209

node z: 228

这个时候比较两者的哈希值,如果大于228,就归到前面的203,相当于整个哈希值就是一个环,对应的映射结果:

node a: 0,1,2

node g: 3,4

node z: 5,6,7,8,9

这个时候加入node n, 就可以算出node n的哈希值:

node n: 216

这个时候对应的数据就会做迁移:

node a: 0,1,2

node g: 3,4

node n: 5,6

node z: 7,8,9

这个时候只有5和6需要做迁移

优势:一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。


为什么要设置虚拟节点

原因1:Hash环的数据倾斜问题
一致性Hash算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题,例如系统中只有两台服务器,其环分布如下:
这里写图片描述

此时必然造成大量数据集中到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上。这样就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。

原因2:雪崩效应

接下来我们来看一下,当有节点宕机时会有什么问题。如下图:

如上图,当B节点宕机后,原本存储在B节点的k1,k2将会迁移到节点C上,这可能会导致很大的问题。如果B上存储的是热点数据,将数据迁移到C节点上, 然后C需要承受B+C的数据,也承受不住,也挂了。。。。然后继续CD都挂了。这就造成了 雪崩效应。

上面会造成雪崩效应的原因分析:
如果不存在热点数据的时候,每台机器的承受的压力是M/2(假设每台机器的最高负载能力为M),原本是不会有问题的,但是,这个时候A服务器由于有热点数据挂了,然后A的数据迁移至B,导致B所需要承受的压力变为M(还不考虑热点数据访问的压力),所以这个失败B是必挂的,然后C至少需要承受1.5M的压力。。。。然后大家一起挂。。。
所以我们通过上面可以看到,之所以会大家一起挂,原因在于如果一台机器挂了,那么它的压力全部被分配到一台机器上,导致雪崩。

怎么解决雪崩问题呢,这时候需要引入虚拟节点来进行解决。
虚拟节点,我们可以针对每个实际的节点,虚拟出多个虚拟节点,用来映射到圈上的位置,进行存储对应的数据。如下图:

如上图:A节点对应A1,A2,BCD节点同理。这时候,如果A节点挂了,A节点的数据迁移情况是:A1数据会迁移到C2,A2数据迁移到D1。这就相当于A的数据被C和D分担了,这就避免了雪崩效应的发送,而且虚拟节点我们可以自定义设置,使其适用于我们的应用。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值