这是一个来自Google的零内存消耗、均匀、快速、简洁的一致性哈希算法 – Jump Consistent Hash

算法的作者是Google的John Lamping和Eric Veach,论文原文链接 – 点这里,一些讨论 – 点这里

整篇文章基于对论文原文的翻译,掺杂自己的一些思想理解,以及对该算法代码的适应场景分析修改,水平有限,难免有偏差:D

一致性哈希(Jump Consistent Hash)算法设计目标

平衡性,把对象均匀地分布在所有桶中。

单调性,当桶的数量变化时,只需要把一些对象从旧桶移动到新桶,不需要做其它移动。

简单理解就是:m个对象,n个桶,每个桶分到的对象应该在m/n个左右,误差不应太大;在桶增加或者桶减少1时,影响到的对象应该也在m/n个左右,其余对象不受影响。

满足这些目标,Jump Consistent Hash的总体设计思路是:计算当bucket数量变化时,有哪些输出需要变化

直接上代码

01
02
03
04
05
06
07
08
09
10
11
12
int32_t ch(int64_t key, int32_t num_buckets)
{
    srand(key);
    int32_t b = -1;
    int32_t j = 0;
    while(j < num_buckets) {
        b = j;
        doubler = rand() / (double)RAND_MAX;
        j = floor((b + 1) / r);
    }
    returnb;
}

测试用例与输出

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
intmain()
{
    printf("======== bucket 4 =========\n");
    for(size_ti = 20000; i < 20020; i++) {
        printf("i:%d b:%lu\n" , i, ch(i, 4));
    }
 
    printf("======== bucket 3 =========\n");
    for(size_ti = 20000; i < 20020; i++) {
        printf("i:%d b:%lu\n" , i, ch(i, 3));
    }
 
    printf("======== bucket 2 =========\n");
    for(size_ti = 20000; i < 20020; i++) {
        printf("i:%d b:%lu\n" , i, ch(i, 2));
    }
 
    return0;
}
 
======= bucket 4 =======   ======= bucket 3 =======   ====== bucket 2 ======
i:20000 b:0                i:20000 b:0                i:20000 b:0
i:20001 b:3                i:20001 b:2                i:20001 b:0
i:20002 b:3                i:20002 b:0                i:20002 b:0
i:20003 b:0                i:20003 b:0                i:20003 b:0
i:20004 b:0                i:20004 b:0                i:20004 b:0
i:20005 b:2                i:20005 b:2                i:20005 b:0
i:20006 b:0                i:20006 b:0                i:20006 b:0
i:20007 b:2                i:20007 b:2                i:20007 b:1
i:20008 b:3                i:20008 b:2                i:20008 b:1
i:20009 b:1                i:20009 b:1                i:20009 b:1
i:20010 b:0                i:20010 b:0                i:20010 b:0
i:20011 b:0                i:20011 b:0                i:20011 b:0
i:20012 b:2                i:20012 b:2                i:20012 b:1
i:20013 b:0                i:20013 b:0                i:20013 b:0
i:20014 b:0                i:20014 b:0                i:20014 b:0
i:20015 b:2                i:20015 b:2                i:20015 b:0
i:20016 b:3                i:20016 b:0                i:20016 b:0
i:20017 b:3                i:20017 b:1                i:20017 b:1
i:20018 b:3                i:20018 b:2                i:20018 b:1
i:20019 b:2                i:20018 b:2                i:20019 b:0

从输出结果来看, 满足了平衡与单调性(这里取得对象数量比较有限,更多的数量会分布更加均匀)。

一致性哈希(Jump Consistent Hash)算法分析

原论文中有大量的概率论证该算法满足平衡与单调性,这里直接忽略,我会撇开概率相关的东西说说我对它直观的理解。

代码的核心部分,srand(key)rand()。这里一个使用线性同余方法产生伪随机数的接口。他的特点是,一旦key确定,那么在这个接口调用里使用rand()产生的随机数必然是相同的。关于线性同余的原理晚上很多介绍,这里测试验证代码:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int32_t test_rand(int64_t key)
{
    srand(key);
    printf("1->key:%llu rand:%d\n",key,rand());
    printf("2->key:%llu rand:%d\n",key,rand());
}
 
intmain()
{
    test_rand(1);
    test_rand(2);
    test_rand(3);
 
    test_rand(3);
    test_rand(2);
    test_rand(1);
    return0;
}
 
1->key:1rand:1804289383
2->key:1rand:846930886
1->key:2rand:1505335290
2->key:2rand:1738766719
1->key:3rand:1205554746
2->key:3rand:483147985
1->key:3rand:1205554746
2->key:3rand:483147985
1->key:2rand:1505335290
2->key:2rand:1738766719
1->key:1rand:1804289383
2->key:1rand:846930886

可以若key一旦确定,那么rand()的伪随机值就是按顺序确定的。这样就保证了,同一个key每次调用ch()后会得到相同的桶索引,除非这个桶索引不在(上例中bucket 4->bucket 3,在bucket 4中b:3的对象会被重新分配)

适用场景分析

比如说我们对于hash桶,存在这样的映射:

0->机器A,1->机器B,2->机器C,3->机器D

当3->机器D从映射表消失后,根据ch()的算法,原来分配到b:3桶的会被重新分配到bucket 0-2。那么问题来了, 若是1->机器B从映射表消失呢?

根据ch()算法,返回还是会重新分配bucket 0-2。那么现在机器映射0,2,3怎么和现在的bucket 0-2对应。外部要维护动态的映射表,实时更新……想想都……

 一些小修改

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//为测试用例而生的结构体
structtest {
    int32_t num;
    boolactive;
};
 
int32_t cch(int64_t key, vector<structtest> &v)
{
    srand(key);
next:
    int32_t b = -1;
    uint32_t j = 0;
    while(j < v.size()) {
        b = j;
        doubler = rand() / (double)RAND_MAX;
        j = floor((b + 1) / r);
    }
 
    if(v[b].active) {
        returnv[b].num;
    }else{
        gotonext;
    }
}

active是需要算法外部维护的,标记这个桶是否还在有效状态,有效-true,无效-false。

测试与输出

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
intmain()
{
 
    vector<structtest> v;
    for(size_ti = 0 ; i < 4; i++) {
        structtest t;
        t.num    = i;
        t.active = true;
        v.push_back(t);
    }
 
    printf("======== bucket 0 1 2 3 =========\n\n");
    for(size_ti = 20000; i < 20020; i++) {
        printf("i:%d b:%lu\n" , i, cch(i, v));
    }
 
    v[0].active = false;
    printf("======== bucket 1 2 3 =========\n\n");
    for(size_ti = 20000; i < 20020; i++) {
        printf("i:%d b:%lu\n" , i, cch(i, v));
    }  
 
    v[1].active = false;
    printf("======== bucket 2 3 =========\n\n");
    for(size_ti = 20000; i < 20020; i++) {
        printf("i:%d b:%lu\n" , i, cch(i, v));
    }
 
    printf("======== bucket 2 3 8 =========\n\n");
    structtest t;
    t.num    = 8;
    t.active = true;
    v.push_back(t);
    for(size_ti = 20000; i < 20020; i++) {
        printf("i:%d b:%lu\n" , i, cch(i, v));
    }
 
    return0;
}
 
===bucket 0 1 2 3===  ===bucket 1 2 3===  ===bucket 2 3===  ===bucket 2 3 8===
i:20000 b:0           i:20000 b:3         i:20000 b:3       i:20000 b:3
i:20001 b:3           i:20001 b:3         i:20001 b:3       i:20001 b:3
i:20002 b:3           i:20002 b:3         i:20002 b:3       i:20002 b:8
i:20003 b:0           i:20003 b:2         i:20003 b:2       i:20003 b:2
i:20004 b:0           i:20004 b:2         i:20004 b:2       i:20004 b:2
i:20005 b:2           i:20005 b:2         i:20005 b:2       i:20005 b:2
i:20006 b:0           i:20006 b:2         i:20006 b:2       i:20006 b:8
i:20007 b:2           i:20007 b:2         i:20007 b:2       i:20007 b:2
i:20008 b:3           i:20008 b:3         i:20008 b:3       i:20008 b:3
i:20009 b:1           i:20009 b:1         i:20009 b:3       i:20009 b:3
i:20010 b:0           i:20010 b:2         i:20010 b:2       i:20010 b:2
i:20011 b:0           i:20011 b:3         i:20011 b:3       i:20011 b:3
i:20012 b:2           i:20012 b:2         i:20012 b:2       i:20012 b:2
i:20013 b:0           i:20013 b:1         i:20013 b:3       i:20013 b:8
i:20014 b:0           i:20014 b:2         i:20014 b:2       i:20014 b:8
i:20015 b:2           i:20015 b:2         i:20015 b:2       i:20015 b:2
i:20016 b:3           i:20016 b:3         i:20016 b:3       i:20016 b:3
i:20017 b:3           i:20017 b:3         i:20017 b:3       i:20017 b:3
i:20018 b:3           i:20018 b:3         i:20018 b:3       i:20018 b:3
i:20019 b:2           i:20019 b:2         i:20019 b:2       i:20019 b:2

如此一来解决bucket_num与机器唯一映射的问题。付出的代价是:1、在有桶状态变更时,受影响对象重新寻桶的循环次数大约增加0.8倍。2、在桶状态时平衡性会略缺失,这点我没有科学的用概率论证,但是从测试用例结果来看是如此。