Hash

哈希表就是 依据关键字可以根据一定的算法(哈希函数)映射到表中的特定位置 的思想建立的表。因此哈希表最大的特点就是可以根据f(K)函数得到其在数组中的索引。

java官方说明中,Object的:

  • equals方法具有自反性、对称性、传递性、一致性,如果两个对象执行equals方法结果为true,则两对象的哈希码应该是相等的。 如果重写equals方法,则一定要重写hashcode方法。
  • hashcode方法是 返回对象的经过处理后的内存地址,由于每个对象的内存地址都不一样,所以哈希码也不一样。此方法为native方法,取决于JVM的内部设计,一般是某种C地址的偏移。
  • 文档中还给出了三条规定:
  1. 在对象没有被修改的前提下,执行多次调用,该hashCode方法必须始终返回相同的整数。
  2. 如果两个对象执行equals方法结果为true,则分别调用hashCode方法产生的整数结果是相等的。
  3. 非必要要求:两个对象执行equals方法结果为false,则分别调用hashCode方法产生的整数结果是不相等的。

第三个要求虽然为非必需,但如果实现,则可以提高散列表的性能。

Object的hash方法

Object类的hashCode. 返回对象的经过处理后的内存地址,由于每个对象的内存地址都不一样,所以哈希码也不一样。这个是native方法,取决于JVM的内部设计,一般是某种C地址的偏移。

String的equals和hashCode方法

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

该函数很简单,以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模,达到了目的——只要字符串的内容相同,返回的哈希码也相同。

  • 选31作为乘子, 它可以被JVM优化:31 * i = (i << 5) - i,并且是一个奇质数
  • unicode编码(万国码,原有的Ascii编码从单字节变成双字节,高位填0)固定占用两个字节,所以,java中char类型的变量也是占用两个字节。
public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

此equals方法包含了"==",双等号比较的是地址,存储地址相同,内容则相同。当地址不同的时候,先验证了比较对象是否为String,接着比较了两个字符串的长度,最后才循环比较每个字符是否相等。

Integer的equals和hashCode方法

    @Override
    public int hashCode() {
        return Integer.hashCode(value);
    }
    //就是返回Integer对象里包含的那个整数的值
    public static int hashCode(int value) {
        return value;
    }

public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

重写一个对象的hashcode

  1. 如果是boolean值,则计算 f ? 1:0;
  2. 如果是byte\char\short\int,则计算 (int)f;
  3. 如果是long值,则计算 (int)(f ^ (f >>> 32));
  4. 如果是float值,则计算 Float.floatToIntBits(f);
  5. 如果是double值,则计算 Double.doubleToLongBits(f),然后返回的结果是long,再用规则(3)去处理long,得到int;
  6. 如果是对象应用,如果equals方法中采取递归调用的比较方式,那么hashCode中同样采取递归调用hashCode的方式。否则需要为这个域计算一个范式,比如当这个域的值为null的时候,那么hashCode 值为0;
  7. 如果是数组,那么需要为每个元素当做单独的域来处理。java.util.Arrays.hashCode 方法包含了8种基本类型数组和引用数组的hashCode计算,算法同上。
    最后再将每个域的散列码相加

哈希碰撞(hash冲突)

  1. 再哈希法,就是出现冲突后采用其他的哈希函数计算,直到不再冲突为止。
  2. 链接地址法,是在出现冲突的地方存储一个链表,所有的同义词记录都存在其中。形象点说就行像是在出现冲突的地方直接把后续的值摞上去。例如HashMap结构。

这很容易理解,因为作为一种可用的散列算法,其位数一定是有限的,也就是说它能记录的文件是有限的——而文件数量是无限的,两个文件指纹发生碰撞的概率永远不会是零。

哈希算法

  1. 求模(%) 但单纯使用求模算法计算之后的结果带有明显的规律性,这种规律将导致算法将能难保证不可逆性。
  2. 异或(^) 再配合移位等运算

流行的hash算法包括 MD5(已被证明不具有强抗碰撞性)、SHA-1 和 SHA-256(碰撞时开销更大)

一个小例子,拍卖会一个人只能出一次价,大家都出好好,价高者得,避免数据提交后有人知道后作弊:最高价+1 这种情况发生,每个人只需要提交自己出价的hash值即可,等到公布后再拿出原价与hash值对应即可。

hash环(一致性hash算法)

先构造一个长度为232的整数环(这个环被称为一致性Hash环),根据节点名称的Hash值(其分布为[0, 232-1])将服务器节点放置在这个Hash环上,然后根据数据的Key值计算得到其Hash值(其分布也为[0, 232-1]),接着在Hash环上顺时针查找距离这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射查找。 ////这种算法解决了普通余数Hash算法伸缩性差的问题,可以保证在上线、下线服务器的情况下尽量有多的请求命中原来路由到的服务器。


Hash_第1张图片
图片.png
  • list+排序
    算出所有节点的hash值放入数组或集合(方便扩展)中,从小到大排列;之后,等待路由的一方,算出自己的hash,然后从list中找到第一hash值大于自己的节点即可,倘若没有,意味此hash值大于所有节点的hash值,那么选list(0),即最小的那个即可。
  • 遍历+list
  1. 服务器节点不排序,其Hash值全部直接放入一个List中
  2. 待路由的节点,算出其Hash值,由于指明了"顺时针"(指的list的顺时针),因此遍历List,比待路由的节点Hash值大的,算出差值并记录,比待路由节点Hash值小的忽略。
  3. 算出所有的差值之后,最小的那个,就是最终需要路由过去的节点。
  • 二叉查找树
    当然我们不能简单地使用二叉查找树,因为可能出现不平衡的情况。平衡二叉查找树有AVL树、红黑树等,这里使用红黑树,选用红黑树的原因有两点:
  1. 红黑树主要的作用是用于存储有序的数据,这其实和第一种解决方案的思路又不谋而合了,但是它的效率非常高
  2. JDK里面提供了红黑树的代码实现TreeMap和TreeSet
    另外,以TreeMap为例,TreeMap本身提供了一个tailMap(K fromKey)方法,支持从红黑树中查找比fromKey大的值的集合,但并不需要遍历整个数据结构。
    使用红黑树,可以使得查找的时间复杂度降低为O(logN),比上面两种解决方案,效率大大提升。

设置虚拟节点

String重写的hashCode()方法在一致性Hash算法中实用价值不大,因为有时服务器节点ip间差别很小,String的hash计算方式导致其值也差距小。得找个算法重新计算HashCode。这种重新计算Hash值的算法有很多,比如CRC32_HASH、FNV1_32_HASH(计算效率会高一些)、KETAMA_HASH(是默认的MemCache推荐的一致性Hash算法)。

/**
 * 带虚拟节点的一致性Hash算法
 * @author 五月的仓颉 http://www.cnblogs.com/xrq730/
 */
public class ConsistentHashingWithVirtualNode
{
    /**
     * 待添加入Hash环的服务器列表
     */
    private static String[] servers = {"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"};
    
    /**
     * 真实结点列表,考虑到服务器上线、下线的场景,即添加、删除的场景会比较频繁,这里使用LinkedList会更好
     */
    private static List realNodes = new LinkedList();
    
    /**
     * 虚拟节点,key表示虚拟节点的hash值,value表示虚拟节点的名称
     */
    private static SortedMap virtualNodes = 
            new TreeMap();
    
    /**
     * 虚拟节点的数目,这里写死,为了演示需要,一个真实结点对应5个虚拟节点
     */
    private static final int VIRTUAL_NODES = 5;
    
    static
    {
        // 先把原始的服务器添加到真实结点列表中
        for (int i = 0; i < servers.length; i++)
            realNodes.add(servers[i]);
        
        // 再添加虚拟节点,遍历LinkedList使用foreach循环效率会比较高
        for (String str : realNodes)
        {
            for (int i = 0; i < VIRTUAL_NODES; i++)
            {
                String virtualNodeName = str + "&&VN" + String.valueOf(i);
                int hash = getHash(virtualNodeName);
                System.out.println("虚拟节点[" + virtualNodeName + "]被添加, hash值为" + hash);
                virtualNodes.put(hash, virtualNodeName);
            }
        }
        System.out.println();
    }
    
    /**
     * 使用FNV1_32_HASH算法计算服务器的Hash值,这里不使用重写hashCode的方法,最终效果没区别 
     */
    private 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;
    }
    
    /**
     * 得到应当路由到的结点
     */
    private static String getServer(String node)
    {
        // 得到带路由的结点的Hash值
        int hash = getHash(node);
        // 得到大于该Hash值的所有Map
        SortedMap subMap = 
                virtualNodes.tailMap(hash);
        // 第一个Key就是顺时针过去离node最近的那个结点
        Integer i = subMap.firstKey();
        // 返回对应的虚拟节点名称,这里字符串稍微截取一下
        String virtualNode = subMap.get(i);
        return virtualNode.substring(0, virtualNode.indexOf("&&"));
    }
    
    public static void main(String[] args)
    {
        String[] nodes = {"127.0.0.1:1111", "221.226.0.1:2222", "10.211.0.1:3333"};
        for (int i = 0; i < nodes.length; i++)
            System.out.println("[" + nodes[i] + "]的hash值为" + 
                    getHash(nodes[i]) + ", 被路由到结点[" + getServer(nodes[i]) + "]");
    }
}

你可能感兴趣的:(Hash)