接触到这个一致性哈希算法是在腾讯音乐的讲座中,用于在线扩容
如图中的例子,本来只有group0和group1,现在要增加一个group2用于推送新的数据,如果使用不满足单调性要求的hash方法,首先向group2推送数据,然后向group0推送数据,如下图所示:
此时,group1还未更新,group0和group2已经更新,在这个时间段,数据docC是丢失的。
但是如果满足单调性,在增加一个group的情况下,原来在group0的docC只会在原来的group0或者新的group2,不可能出现在group1,因此不会出现上述情况。
转自 jump consistent hash的逻辑详解 - 知乎,仅用于自己学习使用,如有侵权请联系删除
目录
Ⅰ. 一致性哈希的特点
在《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在实际使用中的缺点和解决方案。和传统的环形一致性哈希相比,这个算法有两个缺点:
- 不支持设置哈希桶的权重
- 仅能在末尾增加和删除桶,不能删除中间的哈希桶
我们可以采用计算机科学最传统的方法(增加一个中间层)来解决这两个问题:增加一层虚拟桶,使用jump consistent hash来将 分配到虚拟桶中,然后在虚拟桶和实际桶之间建立一个映射关系。这样我们就可以通过映射关系来设置实际桶的权重;也可以在任意位置删除和添加实际桶,只需要维护好映射关系即可。当然,这样做的代价就是,算法本来可以的无内存占用的,现在需要有一块内存来维护映射关系了。
一致性哈希算python包jump-consistent-hash · PyPI