Google Jump Consistent Hash 一致性哈希算法

 接触到这个一致性哈希算法是在腾讯音乐的讲座中,用于在线扩容

如图中的例子,本来只有group0和group1,现在要增加一个group2用于推送新的数据,如果使用不满足单调性要求的hash方法,首先向group2推送数据,然后向group0推送数据,如下图所示:

 此时,group1还未更新,group0和group2已经更新,在这个时间段,数据docC是丢失的。

但是如果满足单调性,在增加一个group的情况下,原来在group0的docC只会在原来的group0或者新的group2,不可能出现在group1,因此不会出现上述情况。

转自 jump consistent hash的逻辑详解 - 知乎,仅用于自己学习使用,如有侵权请联系删除

目录

Ⅰ. 一致性哈希的特点

1.1 一致性

1.2 均匀性

1.3 举例

        Ⅱ. 构造满足一致性,但不满足均匀性的哈希算法

2.1 方法1

2.2 方法2

2.3 方法1和方法2都不满足“均匀性”

Ⅲ. 调整算法中的参数,使其满足等均匀性

3.1 让方法1满足均匀性

3.2 让方法2满足均匀性

总结


Ⅰ. 一致性哈希的特点

在《A Fast, Minimal Memory, Consistent Hash Algorithm》这篇论文中,作者提出,一致性哈希最重要的两个特性是“一致性”和“均匀性”。

1.1 一致性

当给哈希增加一个新桶时,需要对已有的key进行重新映射。一致性的意思是,在这个重新映射的过程中,key要么保留在原来的桶中,要么移动到新增加的桶中。如果key移动到原有的其他桶中,就不满足“一致性”了。

这是一致性哈希区别于传统哈希的特征,传统的哈希在增加一个新桶时,一般会对key进行随机重新的随机映射,key很可能移动到其他原有的桶中。

1.2 均匀性

均匀性是指key会等概率地映射到每个桶中,不会出现某个桶里有大量key,某个桶里key很少甚至没有key的情况。

这是一致性哈希和传统哈希都有的特征。

1.3 举例

理解“一致性”和“均匀性”是理解后文的基础,这里举几个例子来加深理解:

  • 满足“一致性”,不满足“均匀性”的哈希

例1:增加桶4后,所有key仍保留在原有桶中,满足一致性;但桶中的key分部不均匀,不满足均匀性

例2:增加桶4后,所有key移动到新桶中,满足一致性;但桶中的key分部不均匀,不满足均匀性

例3:增加桶4后,原有桶1中的key保持不变,桶2和桶3中的元素移动到新桶中,满足一致性;但桶中的key分部不均匀,不满足均匀性

  • 不满足“一致性”,满足“均匀性”的哈希

例4:增加桶4后,各key均匀分布,满足均匀性;但key_4从原有的桶1移动到桶2,不满足一致性

  • 同时满足“一致性”和“均匀性”的哈希

例5:增加桶后,各key均匀分布,满足均匀性;并且key没有在原有的桶中移动,也满足一致性

Ⅱ. 构造满足一致性,但不满足均匀性的哈希算法

我们先看看“一致性”的要求:当增加新桶时,原有桶中的key要么保留在原有的桶中,要么移动到新增的这个桶中。

假设一致性哈希函数为  ,其中 key为要放入元素的键值,n 为桶的个数,函数返回值是给 key分配的桶id(从0开始),返回值的范围为 [0,n-1]。那么根据“一致性”的要求,有如下的递推公式

 

有了这个递推公式,我们就很方便实现满足“一致性”的哈希算法了。

2.1 方法1

直接使用这个递推公式。开始桶的总数为1,所有的key都放在第0个桶中。然后每增加一个桶,生成一个随机数,当这个随机数为奇数时,将key放在保持在原始桶中,当这个key为偶数时,将key移动到新增的桶中。

具体C++代码如下:

unsigned int ch(unsigned int key, unsigned int n)
{
    srand(key);

    unsigned int id = 0; // 桶数为1时,所有key放到第0个桶中
    for (unsigned int i = 1; i < n; i++)
    {
        // 每增加一个桶,生成一个随机数
        if (rand() % 2 == 1)
        {
            id = id; // 如果随机数为奇数,key仍然保留在原来的桶中
        }
        else
        {
            id = i; // 如果随机数为偶数,将key移动到新分配的桶中
        }
    }

    return id;
}

注意,由于使用key作为随机数的种子,因此一旦key和n确定了,函数的返回值也就是确定的。并且当函数参数为n和参数为n-1时,循环过程中的前面几步生成的随机数都是一样的。

2.2 方法2

开始桶的总数为1,所有的key都放在第0个桶中,同时生成一个大于当前桶数的随机数。每增加一个新桶时,判断当前桶总数是否超过这个随机数。如果未超过(桶数小于或等于这个随机数),则将key保留在原来的桶中;如超过,则将key移动到新增加的桶中,同时重新生成一个大于当前桶数的随机数,后续增加新桶时,使用和前面相同的逻辑进行判断。

和方法1一样,我们也使用key作为随机数的种子,因此一旦key和n确定了,函数的返回值也就是确定的。

具体C++代码如下:

unsigned int ch(unsigned int key, unsigned int n)
{
    srand(key);
    unsigned int id = 0;
    unsigned int fence = rand();
    while (n > fence)
    {
        id = fence;
        fence = id + rand();
    }

    return id;
}

这个代码初看可能不太直观,我们一步一步来分析:

  • 当n=1时,可以看出函数的返回值id是0,同时假设生成一个随机数fence=3
  • 当n=2, 3时,因为随机数种子不变,所以开始生成的随机数fence也是3,这个时候函数的返回值id仍然是0
  • 当n=4时,因为因为随机数种子不变,开始fence=3,所以n>fence,进入循环中。进入循环后id=3,并假设生成的新fence=3+5=8。此时n<新生成的fence,跳出循环返回id=3
  • 当n=5, 6, 7, 8时,因为因为随机数种子不变,开始fence=3,所以n>fence,进入循环中。进入循环后id=3,新生成的fence=8。此时n=新生成的fence,跳出循环返回id=3
  • 当n=9时,前面都是重复n=5, 6, 7, 8的步骤。在最后一步时,因为n=9,fence=8,满足n>fence,会再次进入循环,此时id=8,假设新生成fence=8+2=10。此时会跳出循环返回id=8
  • ……

可以看出,这个方法也是满足递推公式的。当输入参数为n时,函数的返回值只有两个分支:要么和参数为n-1时相同,要么函数的返回值是n-1。而这两个方法,正好对应论文《A Fast, Minimal Memory, Consistent Hash Algorithm》中O(N)和O(logN)的两个算法,区别仅在于生成随机数的方式、函数返回值走那个分支的判断方法。

2.3 方法1和方法2都不满足“均匀性”

接下来我们分析下这两个方法是否满足“均匀性”。哈希的“均匀性”要求对于任意的key,当桶数为n时,key被分配到任意桶中的概率都是1/n。

对于方法1,我们分析下当桶的总数为3时,key被分配到第0个桶中的概率:

当桶数为3时,循环会执行2次,如果要key被分配到第0个桶,要求两次生成的随机数都是奇数,其概率是1/4。很明显不满足“均匀性”

对于方法2,我们分析下当桶的总数为2时,key被分配到第0个桶中的概率:

当桶的总数为2时,如果生成的随机数fence大于或等于2,那么不会进入循环体,key也就被分配到第0个桶中了。因为rand()的返回值均匀分布在[0, RAND_MAX]之间,因此生成的随机数fence大于或等于2的概率是非常大的,几乎接近1。因此这个情况下key被分配到第0个桶中的概率接近1。也不满足“均匀性”。

Ⅲ. 调整算法中的参数,使其满足等均匀性

这部分,我们并不去推导如何设计算法中参数,使满足均匀性。而是直接使用论文中的参数,并证明这个参数能满足均匀性。也就是说,本部分不讲推导过程,仅讲证明过程。

哈希的“均匀性”要求对于任意的key,当桶数为n时,key被分配到任意桶中的概率都是1/n。

3.1 让方法1满足均匀性

先回顾下方法1的思路。

方法1:直接使用这个递推公式。开始桶的总数为1,所有的key都放在第0个桶中。然后每增加一个桶,生成一个随机数,当这个随机数为奇数时,将key放在保持在原始桶中,当这个key为偶数时,将key移动到新增的桶中

根据前面的分析,由于增加桶时,key移动到新桶和保留在原始桶中的概率是1/2,因此不满足“均匀性”。那为了让其满足“均匀性”,我们需要调整key移动到新桶和保留在原始桶中的概率。

假设当前桶数为k,如果新增加一个桶,key移动到新桶的概率为1/(k+1),那么算法就可以满足“均匀性”了。我们可以一步一步进行证明:

首先对n=1、2、3这个特殊情况进行推导:

  • 首先,当桶总数n=1时,key分配到第0个桶中的概率是1
  • 新增一个桶,此时n=2,key被分配到新桶(第1个桶)中的概率是1/2,保留在原桶中的概率也是1/2
  • 再新增一个桶,此时n=3,key被分配到新桶(第2个桶)中的概率是1/3,保留原桶(第0或1个桶)中的概率是1/2 * 2/3 = 1/3

然后我们可以有更通用的推导:

  • 当n=k时,key被分配到每个桶中的概率是1/n
  • 再新增一个桶,此时n=k+1,key被分配到新桶(第k个桶)中的概率是1/(k+1),保留原桶(第0或1或……或k-1个桶)中的概率是1/k * k/(k+1) = 1/(k+1)。此时key被分配到每个桶的概率仍然为1/n

因此方法1的代码修改如下:

int ch(int key, int n)
{
    random.seed(key);
    int id = 0;
    for (int j = 1; j < n; j++)
    {
        if (random.next() < 1.0/(j+1))
        {
            id = j;
        }
    }
    return id;
}

3.2 让方法2满足均匀性

先回顾下方法2的思路

方法2:开始桶的总数为1,所有的key都放在第0个桶中,同时生成一个大于当前桶数的随机数。每增加一个新桶时,判断当前桶总数是否超过这个随机数。如果未超过(桶数 小于或等于这个随机数),则将key保留在原来的桶中;如超过,则将key移动到新增加的桶中,同时重新生成一个大于当前桶数的随机数,后续增加新桶时,使用和前面相同的逻辑进行判断。

 

 

 

int ch(int key, int n)
{
    random.seed(key);
    int b = 0;
    int f = 0;
    while (f < n)
    {
        b = f;
        r = random.next();
        f = floor((b+1)/r)
    }
    return b;
}

总结

本文从另外一个角度来解释了jump consistent hash,希望能够帮助大家理解这个很厉害的算法。

另外,在最后也提一下jump consistent hash在实际使用中的缺点和解决方案。和传统的环形一致性哈希相比,这个算法有两个缺点:

  1. 不支持设置哈希桶的权重
  2. 仅能在末尾增加和删除桶,不能删除中间的哈希桶

我们可以采用计算机科学最传统的方法(增加一个中间层)来解决这两个问题:增加一层虚拟桶,使用jump consistent hash来将   分配到虚拟桶中,然后在虚拟桶和实际桶之间建立一个映射关系。这样我们就可以通过映射关系来设置实际桶的权重;也可以在任意位置删除和添加实际桶,只需要维护好映射关系即可。当然,这样做的代价就是,算法本来可以的无内存占用的,现在需要有一块内存来维护映射关系了。

一致性哈希算python包jump-consistent-hash · PyPI

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值