一致性哈希算法

引入

我们该访问谁?

当我们的本地缓存不存在Key时,需要到其他peer节点查找,那么应该去哪个节点呢?
假设我们随机选择,这样选到正确节点的概率是1/N(N为节点个数),我们大概率选择不到正确的节点,则导致我们大概率要访问数据库,这样的开销很大;
是否存在一种方法,对于相同的Key,我们每次定位到相同的节点?
哈希算法可以!对Key使用哈希算法,比如,我们可以简单的自定义这样的哈希算法:把每个字符的和相加对N取模;这样就解决了我该访问谁的问题!

N发生变化怎么办?

如果N减少变为N-1,那么会发生什么?之前我们映射的Key,再次对N取模后,就无法访问到正确的节点了。如何解决这个问题呢?
一致性哈希可以!
一致性哈希算法是把节点映射到一个1~(1^32 - 1)的环上,我们对每个Key的哈希值顺时针在环上找到离它最近的节点,这种情况下,当有新增节点或减少节点时,只会影响该节点附近的节点;通过环+找离自己最近的节点,我们就解决了节点个数发生变化时哈希值取模后映射失败的问题。
但是,我们很快会发现,如果节点个数较少的情况下,部分节点会覆盖很大空间的Key,这样每个节点的负载是不均衡的,这个问题又该如何解决呢?

如何实现负载均衡?

为了解决节点个数少的问题,我们引入虚拟节点的概念,所谓虚拟节点,就是这个节点本身不存储Key,而是某个实体节点的”替身“,当我们选择到该节点时,我们实际应该访问的是它的”本体“,我们对每个实体节点,随机在环上散列一定数量的虚拟节点,这样就可以实现节点的负载均衡!

源码解析

consistenthash.go

结构体Map

type Map struct {
    hash     Hash
    replicas int // The number of virtual nodes for every key
    keys     []int // Sorted
    hashMap  map[int]string
}
crc32.ChecksumIEEE

初始化方法

func New(replicas int, fn Hash) *Map {
    m := &Map{
        replicas: replicas,
        hash:     fn,
        hashMap:  make(map[int]string),
    }
    if m.hash == nil {
        m.hash = crc32.ChecksumIEEE
    }
    return m
}
Add(keys ...string)

参数是keys,...是golang的语法糖,表示不确定数量的参数,或者可以打散Slice;

for _, key := range keys {
        for i := 0; i < m.replicas; i++ {
            hash := int(m.hash([]byte(strconv.Itoa(i) + key)))
            m.keys = append(m.keys, hash)
            m.hashMap[hash] = key
        }
    }
    sort.Ints(m.keys)

对每一个加入的新节点key,我们在环上生成replicas个虚拟节点(注意这里key本事的哈希值不加入环中作为一个node),生成方法是调用哈希函数hash;然后把它们全部加入keys;记得我们要把这个虚拟节点映射的实体节点key;最后对节点列表排序;

Get(key string) string

包含一个参数key,计算该key的哈希值,然后找到离它最近的虚拟节点,返回该虚拟节点的实体key。

    if hash1.Get("0Bill") != hash2.Get("0Bill") ||
        hash1.Get("0Bob") != hash2.Get("0Bob") ||
        hash1.Get("0Bonny") != hash2.Get("0Bonny") {
        t.Errorf("Direct matches should always return the same entry")
    }