场景设定
- 真实节点(服务器):
NodeA
和NodeB
- 虚拟节点副本数:
m.replicas = 3
- 数据键:
"Data1"
、"Data2"
、"Data3"
我们将:
- 初始化一致性哈希环。
- 添加真实节点,并生成虚拟节点。
- 存储数据,将数据键映射到真实节点。
- 查找数据,根据数据键找到对应的真实节点。
- 模拟节点的增减,观察数据映射的变化。
步骤 1:初始化一致性哈希环
首先,创建一个一致性哈希环的结构体 Map
,包含以下属性:
hash
:哈希函数。replicas
:虚拟节点的数量(每个真实节点的副本数)。keys
:存储虚拟节点的哈希值,有序排列。hashMap
:映射虚拟节点哈希值到真实节点。
type Map struct {hash HashFunc // 哈希函数replicas int // 虚拟节点副本数keys []int // 排序后的虚拟节点哈希值hashMap map[int]string // 虚拟节点哈希值到真实节点的映射
}
假设我们使用 Go 语言的 crc32.ChecksumIEEE
作为哈希函数。
func New(replicas int, fn HashFunc) *Map {return &Map{hash: fn,replicas: replicas,hashMap: make(map[int]string),}
}
初始化哈希环:
hashRing := New(3, crc32.ChecksumIEEE)
步骤 2:添加真实节点并生成虚拟节点
调用 Add
方法,将真实节点 NodeA
和 NodeB
添加到哈希环中。
hashRing.Add("NodeA", "NodeB")
Add
方法的实现:
func (m *Map) Add(keys ...string) {for _, key := range keys {for i := 0; i < m.replicas; i++ {virtualNodeID := strconv.Itoa(i) + keyhash := int(m.hash([]byte(virtualNodeID)))m.keys = append(m.keys, hash)m.hashMap[hash] = key}}sort.Ints(m.keys)
}
执行过程:
- 为每个真实节点生成
3
个虚拟节点。 - 计算每个虚拟节点的哈希值,并建立映射关系。
示例:
-
对于
NodeA
:- 虚拟节点
"0NodeA"
,哈希值hash("0NodeA")
- 虚拟节点
"1NodeA"
,哈希值hash("1NodeA")
- 虚拟节点
"2NodeA"
,哈希值hash("2NodeA")
- 虚拟节点
-
对于
NodeB
:- 虚拟节点
"0NodeB"
,哈希值hash("0NodeB")
- 虚拟节点
"1NodeB"
,哈希值hash("1NodeB")
- 虚拟节点
"2NodeB"
,哈希值hash("2NodeB")
- 虚拟节点
假设计算得到以下哈希值:
虚拟节点标识符 | 哈希值(假设) | 真实节点 |
---|---|---|
"0NodeA" | 105 | NodeA |
"1NodeA" | 215 | NodeA |
"2NodeA" | 325 | NodeA |
"0NodeB" | 125 | NodeB |
"1NodeB" | 235 | NodeB |
"2NodeB" | 345 | NodeB |
- 将哈希值添加到
m.keys
列表中。 - 建立哈希值到真实节点的映射
m.hashMap
。
排序后的 m.keys
列表:
m.keys = [105, 125, 215, 235, 325, 345]
步骤 3:存储数据
数据键:
"Data1"
"Data2"
"Data3"
存储过程:
对于每个数据键,执行以下步骤:
- 计算数据键的哈希值。
- 在
m.keys
中查找第一个大于等于该哈希值的虚拟节点。 - 通过
m.hashMap
获取对应的真实节点。
实现代码:
func (m *Map) Get(key string) string {if len(m.keys) == 0 {return ""}hash := int(m.hash([]byte(key)))idx := sort.Search(len(m.keys), func(i int) bool {return m.keys[i] >= hash})return m.hashMap[m.keys[idx%len(m.keys)]]
}
具体执行:
-
数据键
"Data1"
:-
计算哈希值:
hash("Data1") = 220 // 假设哈希值为 220
-
查找虚拟节点:
m.keys
列表:[105, 125, 215, 235, 325, 345]
- 使用
sort.Search
,找到索引idx = 3
(因为m.keys[3] = 235 >= 220
)
-
获取真实节点:
node = m.hashMap[m.keys[3]] // m.keys[3] = 235 node = "NodeB"
-
存储位置:
"Data1"
存储在NodeB
-
-
数据键
"Data2"
:-
计算哈希值:
hash("Data2") = 330 // 假设哈希值为 330
-
查找虚拟节点:
- 找到索引
idx = 5
(因为m.keys[5] = 345 >= 330
)
- 找到索引
-
获取真实节点:
node = m.hashMap[m.keys[5]] // m.keys[5] = 345 node = "NodeB"
-
存储位置:
"Data2"
存储在NodeB
-
-
数据键
"Data3"
:-
计算哈希值:
hash("Data3") = 90 // 假设哈希值为 90
-
查找虚拟节点:
- 找到索引
idx = 0
(因为m.keys[0] = 105 >= 90
)
- 找到索引
-
获取真实节点:
node = m.hashMap[m.keys[0]] // m.keys[0] = 105 node = "NodeA"
-
存储位置:
"Data3"
存储在NodeA
-
步骤 4:查找数据
当我们需要读取数据时,使用相同的 Get
方法,根据数据键找到对应的真实节点,然后从该节点获取数据。
示例:
-
查找
"Data1"
:- 计算哈希值
hash("Data1") = 220
- 查找到虚拟节点哈希值
235
- 映射到真实节点
NodeB
- 从
NodeB
获取"Data1"
数据
- 计算哈希值
步骤 5:模拟节点的增减
情况一:新增节点 NodeC
-
添加
NodeC
:hashRing.Add("NodeC")
-
为
NodeC
创建虚拟节点:虚拟节点标识符 哈希值(假设) 真实节点 "0NodeC"
160
NodeC
"1NodeC"
270
NodeC
"2NodeC"
380
NodeC
-
更新
m.keys
列表并排序:m.keys = [105, 125, 160, 215, 235, 270, 325, 345, 380]
-
重新映射数据:
-
"Data1"
(哈希值 220):- 新的索引
idx = 4
(m.keys[4] = 235 >= 220
) - 真实节点仍为
NodeB
- 新的索引
-
"Data2"
(哈希值 330):- 新的索引
idx = 6
(m.keys[6] = 325 < 330
,但m.keys[7] = 345 >= 330
) - 真实节点为
NodeB
(无变化)
- 新的索引
-
"Data3"
(哈希值 90):- 索引
idx = 0
(m.keys[0] = 105 >= 90
) - 真实节点为
NodeA
(无变化)
- 索引
-
-
添加新数据
"Data4"
(哈希值 280):- 计算哈希值
hash("Data4") = 280
- 索引
idx = 6
(m.keys[6] = 325 >= 280
) - 真实节点为
NodeA
"Data4"
存储在NodeA
- 计算哈希值
情况二:移除节点 NodeB
-
从哈希环中移除
NodeB
:hashRing.Remove("NodeB")
-
Remove
方法的实现(简化版):func (m *Map) Remove(key string) {for i := 0; i < m.replicas; i++ {virtualNodeID := strconv.Itoa(i) + keyhash := int(m.hash([]byte(virtualNodeID)))idx := sort.Search(len(m.keys), func(i int) bool {return m.keys[i] == hash})if idx < len(m.keys) && m.keys[idx] == hash {m.keys = append(m.keys[:idx], m.keys[idx+1:]...)}delete(m.hashMap, hash)} }
-
更新后的
m.keys
列表:m.keys = [105, 160, 215, 270, 325, 380]
-
重新映射数据:
-
"Data1"
(哈希值 220):- 新的索引
idx = 3
(m.keys[3] = 270 >= 220
) - 真实节点为
NodeC
(原来是NodeB
)
- 新的索引
-
"Data2"
(哈希值 330):- 新的索引
idx = 5
(m.keys[5] = 380 >= 330
) - 真实节点为
NodeC
(原来是NodeB
)
- 新的索引
-
"Data3"
(哈希值 90):- 索引
idx = 0
(m.keys[0] = 105 >= 90
) - 真实节点为
NodeA
(无变化)
- 索引
-
-
结果:
"Data1"
和"Data2"
重新映射到NodeC
"Data3"
仍然在NodeA
总结
通过以上模拟,我们可以看到:
- 一致性哈希算法在节点增减时,只有部分数据需要重新映射,保证了数据迁移的最小化。
- 虚拟节点的引入,使得数据在真实节点间分布更加均衡,避免了数据倾斜。
- 通过哈希环的顺时针查找,简单高效地实现了数据键到节点的映射。
注意事项:
- 哈希函数的选择:应使用分布性良好的哈希函数,避免哈希冲突。
- 虚拟节点数量的设定:虚拟节点越多,数据分布越均衡,但也增加了哈希环的维护开销。
- 一致性:在分布式环境中,各节点应共享相同的哈希环视图,保证一致性。