引入
我们该访问谁?
当我们的本地缓存不存在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")
}