场景设定
- 真实节点(服务器):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"160NodeC"1NodeC"270NodeC"2NodeC"380NodeC
-  更新 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
 
总结
通过以上模拟,我们可以看到:
- 一致性哈希算法在节点增减时,只有部分数据需要重新映射,保证了数据迁移的最小化。
- 虚拟节点的引入,使得数据在真实节点间分布更加均衡,避免了数据倾斜。
- 通过哈希环的顺时针查找,简单高效地实现了数据键到节点的映射。
注意事项:
- 哈希函数的选择:应使用分布性良好的哈希函数,避免哈希冲突。
- 虚拟节点数量的设定:虚拟节点越多,数据分布越均衡,但也增加了哈希环的维护开销。
- 一致性:在分布式环境中,各节点应共享相同的哈希环视图,保证一致性。
