hash() 在各个场景下的实现

人走茶凉 提交于 2020-02-26 19:13:59

常用的hash算法有哪些?

• 加法Hash:把输入元素一个一个的加起来构成最后的结果

• 位运算Hash:这类型Hash函数通过利用各种位运算(常见的是移位和异或)来充分的混合输入元素

• 乘法Hash:这种类型的Hash函数利用了乘法的不相关性(乘法的这种性质,最有名的莫过于平方取头尾的随机数生成算
法,虽然这种算法效果并不好);jdk5.0里面的String类的hashCode()方法也使用乘法Hash;32位FNV算法

• 除法Hash:除法和乘法一样,同样具有表面上看起来的不相关性。不过,因为除法太慢,这种方式几乎找不到真正的应用

• 查表Hash:查表Hash最有名的例子莫过于CRC系列算法。虽然CRC系列算法本身并不是查表,但是,查表是它的一种最快
的实现方式。查表Hash中有名的例子有:Universal Hashing和Zobrist Hashing。他们的表格都是随机生成的。

• 混合Hash:混合Hash算法利用了以上各种方式。各种常见的Hash算法,比如MD5、Tiger都属于这个范围。它们一般很少
在面向查找的Hash函数里面使用
 

 

Object类的hashCode.

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

 

String类的hashCode

    根据String类包含的字符串的内容,根据 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]  的计算,返回哈希码,只要字符串的内容相同,返回的哈希码也相同。

public int hashCode() {
    int h = hash;
    if (h == 0) {
        int off = offset;
        char val[] = value;
        int len = count;

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

    为什么选择31作为乘积因子,而且没有用一个常量来声明?主要原因有两个:

  ①、31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。

  ②、31可以被 JVM 优化,31 * i = (i << 5) - i。因为移位运算比乘法运行更快更省性能。

 

Integer

    返回的哈希码就是Integer对象里所包含的那个整数的数值,例如Integer i1=new Integer(100), i1.hashCode的值就是100 。由此可见,2个一样大小的Integer对象,返回的哈希码也一样。

 

Long

    value ^ (value >>> 32)

 

Boolean

    value ? 1231 : 1237

 

int,char这样的基本数据类型

    它们不需要hashCode,如果需要存储时,将进行自动装箱操作,然后再存储。

 

hashMap 的 hash():

    在hashMap 中,key在数组中的位置计算:需要根据key的 hash值 对其数组长度进行取模得到。

    需要注意:元素的 hash 值,不是简单的使用key的 hashCode()。

    JDK8 中:

        将key的hashCode的二进制表示: 高16位 与 低16位进行异或运算 的得到hash值。

    

    JDK 8 以前:

    

 

hashtable

    在hashtable中,key在数组中的位置计算:

int hash = key.hashCode();

int index = (hash & 0x7FFFFFFF) % tab.length;

 

一致性hash

     构造一个2^32的整数环,即0~(2^32-1)的数字空间,形成一个环,起点为0,终点为2^32-1。

    

    计算机器的ip的hash值,再对 2^32 取模。即:hash(IP) % 2^32,会映射到圆上的一点。

    将存储的key进行hash(key)2^32, 它的值在圆上映射的位置开始,顺时针方向找到的第一个机器节点,即为存储该key的机器节点。

    如果 新增/删除 服务器,则对受影响的key 进行重新分配即可。

    虚拟节点:解决 数据倾斜 的情况,防止出现大量数据存放在某一个 node 的情形。 

        此时,将真实节点计算多个哈希形成多个虚拟节点并放置到哈希环上真实节点不放置到哈希环上,只有虚拟节点才会放上去。 每次根据key得到存储对应的某个虚拟节点,还需要做一次虚拟节点向真实节点的映射处理。

    hash()方法的选用没有特别指明,但是需要保证出现hash碰撞的可能性要尽可能的小。重新计算Hash值的算法有很多,比如CRC32_HASH、FNV1_32_HASH、KETAMA_HASH等,其中KETAMA_HASH是默认的MemCache推荐的一致性Hash算法,用别的Hash算法也可以,比如FNV1_32_HASH算法的计算效率就会高一些。

     /**
      * 使用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;
     }

 

hash 槽

     Redis 集群的键空间被分割为 16384 个 hash 槽(slot), 集群的最大节点数量也是 16384 个

    一个 Redis Cluster包含16384(0~16383)个哈希槽,存储在Redis Cluster中的所有键都会被映射到这些slot中。

    所有的master节点都会有一个槽区比如:0~1000,槽数是可以迁移的。 master节点的slave节点不分配槽,只拥有读权限。

     key的定位规则:根据CRC-16(key)%16384的值来判断属于哪个槽区,从而判断该key属于哪个节点 。CRC16(key) 是用于计算key的 CRC16校验和

    当前集群有3个节点,槽默认是平均分的:
        节点 A (6381)包含 0 到 5499号哈希槽.
        节点 B (6382)包含5500 到 10999 号哈希槽.
        节点 C (6383)包含11000 到 16383号哈希槽.

        如果想新添加一个节点D,我需要从节点 A, B, C中得部分槽到D上。

        如果想移除节点A,需要将A中的槽,移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可。

        由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量,都不会造成集群不可用的状态。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!