​ 散列算法(Hash Algorithm),又称哈希算法,杂凑算法,是一种从任意文件中创造小的数字「指纹」的方法。也可以理解为空间映射函数,是从一个非常大的取值空间映射到一个非常小的取值空间,由于不是一对一的映射,Hash 函数转换后不可逆,意思是不可能通过逆操作和 Hash 值还原出原始的值。在互联网环境中有非对称加密、文件签名、Hash表寻址算法等应用。

Hash算法类型

直接寻址法:按照该值作为三列地址进行查找。

除留余数法(数据取模法):对该值按照散列表的长度来取余。

处理Hash冲突时的方法:

​ 开放寻址法:冲突后向前或者向后寻找空位作为当前地址。

​ 再散列法:准备多个散列函数,冲突后使用其他散列函数计算。

​ 拉链法:冲突后,将冲突的数据放到一个链表中。

Hash算法在分布式环境中的应用

  • 请求的负载均衡(如nginx的ip_hash策略)

    Nginx负载均衡的经典应用就是通过ip地址或者sessionid进行计算哈希值,哈希值与服务器数量进行取模运算,得到的值就是当前请求应该被路由到的服务器编号,如此,同一个客户端ip发送过来的请求就可以路由到同一个目标服务器,实现会话粘滞。

    如果不使用hash算法,就需要非常维护非常大的映射表,造成内存浪费,且服务器上下线后不利于维护。

  • 分布式存储

    比如Redis集群模式下,数据应该存储在哪个redis应用里呢?通过Hash算法就可以解决这个问题。

普通HASH算法带来的数据迁移问题

​ 在普通HASH算法下,,如果hash表扩容/缩容,所有的数据均需要重新计算一遍来确定寻址位置。在大量数据的环境下,这会带来很多问题。比如Nginx下的ip_hash负载均衡策略,它的目的是将同一个客户端的请求路由到同一台服务器上,当服务器扩容或者缩容时,使用普通hash算法则使得所有客户端重新计算路由的服务器编号,大量的客户端session失效,这在互联网环境下是难以接受的。

一致性HASH算法

​ 有一条直线,直线开头和结尾分别定为为1和2的32次方减1,这相当于一个地址。

一致性Hash直线

​ 对于这样一条线,弯过来构成一个圆环形成闭环,这样的一个圆环称为hash环。

一致性hash环

​ 我们把服务器的ip或者主机名求hash值然后对应到hash环上,那么针对客户端用户,也根据它的ip进行hash求值,对应到环上某个位置,然后如何确定一个客户端路由到哪个服务器处理呢?按照顺时针方向找最近的服务器节点。比如服务器A在hash环上对应的值为100,服务器B在hash环上对应的值为200,那么经过hash计算后值为大于100小于200的均路由到服务器B上。

  1. 当服务下线时

    ​ 会按照顺时针将请求路由至下一个服务器。

  2. 当服务上线时

    ​ 会将当前节点逆时针至上一台服务器的请求路由至本服务器。

总的来说Hash环按照服务器的数量划分成了不同的段,每台服务器负责一段,增减服务器均只影响部分数据,具有良好的容错性和扩展性。

​ 但分段也造成了数据倾斜问题,即负载不均衡的问题,有的服务器器不堪重负,有的服务器资源闲置浪费。这就需要我们尽可能的使服务器分布均衡。当我们的服务器节点较少时,可以采用填充虚拟节点的方式来平衡分段。具体的做法是给每个虚拟节点分配一个真实的物理节点,当请求到虚拟节点时,路由至附带的真实节点。

一致性HASH算法的简单实现

不含虚拟节点

public class ConsistentHashNoVirtual {

    public static void main(String[] args) {
        //step1 初始化:把服务器节点IP的哈希值对应到哈希环上
        // 定义服务器ip
        String[] tomcatServers = new String[]{"123.111.0.0", "123.101.3.1", "111.20.35.2", "123.98.26.3"};
        SortedMap<Integer, String> hashServerMap = new TreeMap<>();
        for (String tomcatServer : tomcatServers) {
            // 求出每⼀个ip的hash值,对应到hash环上,存储hash值与ip的对应关系
            int serverHash = Math.abs(tomcatServer.hashCode());
            // 存储hash值与ip的对应关系
            hashServerMap.put(serverHash, tomcatServer);
        }

        //step2 针对客户端IP求出hash值
        // 定义客户端IP
        String[] clients = new String[]{"10.78.12.3", "113.25.63.1", "126.12.3.8"};
        for (String client : clients) {
            int clientHash = Math.abs(client.hashCode());
            
            //step3 针对客户端,找到能够处理当前客户端请求的服务器(哈希环上顺时针最近)
            
            // 根据客户端ip的哈希值去找出哪⼀个服务器节点能够处理()| tailMap返回大于等于Key的map
            SortedMap<Integer, String> integerStringSortedMap = hashServerMap.tailMap(clientHash);
            if (integerStringSortedMap.isEmpty()) {
                // 取哈希环上的顺时针第一台服务器
                Integer firstKey = hashServerMap.firstKey();
                System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
            } else {
                Integer firstKey = integerStringSortedMap.firstKey();
                System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
            }
        }
    }
}

计算结果:

==========>>>>客户端:10.78.12.3 被路由到服务器:111.20.35.2
==========>>>>客户端:113.25.63.1 被路由到服务器:123.98.26.3
==========>>>>客户端:126.12.3.8 被路由到服务器:111.20.35.2

含有虚拟节点

public class ConsistentHashWithVirtual {

    // 定义服务器ip
    public static final String[] TOMCAT_SERVERS = new String[]{"123.111.0.0", "123.101.3.1", "111.20.35.2", "123.98.26.3"};
    // 定义客户端IP
    public static final String[] CLIENT_IPS = new String[]{"10.78.12.3", "113.25.63.1", "126.12.3.8"};
    // 定义针对每个真实服务器虚拟出来⼏个节点
    public static final int VIRTAUL_COUNT = 3;

    public static void main(String[] args) {

        //step1 初始化:把服务器节点IP的哈希值对应到哈希环上
        SortedMap<Integer, String> hashServerMap = new TreeMap<>();
        for (String tomcatServer : TOMCAT_SERVERS) {

            // 求出每⼀个ip的hash值,对应到hash环上,存储hash值与ip的对应关系
            int serverHash = Math.abs(tomcatServer.hashCode());

            // 存储hash值与ip的对应关系
            hashServerMap.put(serverHash, tomcatServer);
            // 处理虚拟节点
            for (int i = 0; i < VIRTAUL_COUNT; i++) {
                int virtualHash = Math.abs((tomcatServer + "#" + i).hashCode());
                hashServerMap.put(virtualHash, "----由虚拟节点" + i + "映射过来的请求:" + tomcatServer);
            }
        }

        //step2 针对客户端IP求出hash值
        for (String client : CLIENT_IPS) {

            int clientHash = Math.abs(client.hashCode());

            //step3 针对客户端,找到能够处理当前客户端请求的服务器(哈希环上顺时针最近)
            // 根据客户端ip的哈希值去找出哪⼀个服务器节点能够处理()| tailMap返回大于等于Key的map
            SortedMap<Integer, String> integerStringSortedMap = hashServerMap.tailMap(clientHash);
            if (integerStringSortedMap.isEmpty()) {

                // 取哈希环上的顺时针第⼀台服务器
                Integer firstKey = hashServerMap.firstKey();
                System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
            } else {
                Integer firstKey = integerStringSortedMap.firstKey();
                System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
            }
        }
    }
}

计算结果:

==========>>>>客户端:10.78.12.3 被路由到服务器:111.20.35.2
==========>>>>客户端:113.25.63.1 被路由到服务器:----由虚拟节点2映射过来的请求:111.20.35.2
==========>>>>客户端:126.12.3.8 被路由到服务器:----由虚拟节点0映射过来的请求:123.101.3.1