一致性哈希算法的核心目的是在分布式系统中,当服务器节点增加或减少时,尽可能减少需要重新映射的数据量,从而保持系统稳定性。
哈希环:算法将整个哈希值空间组织成一个首尾相接的环(通常是
0~
2^32 - 1)。系统(以我的在线预约系统为例)中的服务器节点(如不同的预约处理服务实例)和需要存储的数据(如预约请求)都通过哈希函数映射到这个环上。
数据定位:当需要为一条数据(例如一个预约ID)找到对应的处理节点时,算法会从数据在环上的位置出发,顺时针查找,遇到的第一台服务器即为目标节点。
虚拟节点:为解决实际节点过少时可能引发的数据倾斜(即部分节点负载过重)问题,引入了虚拟节点机制。一个物理节点可以对应多个虚拟节点分布在全环上,通过增加节点在环上的分布密度,使数据分配更均匀,负载更平衡。虚拟节点的数量通常设置为32甚至更大。
|
特性 |
传统哈希 |
一致性哈希 |
|---|---|---|
|
伸缩性 |
节点变动时,大部分数据需要重新映射,迁移成本高(O(M)) |
仅影响相邻节点数据,迁移量小(K/N,其中K是关键字的数量,N是槽位数量) |
|
数据分布 |
依赖哈希函数本身 |
通过虚拟节点技术,可使分布更均匀 |
|
单调性 |
不保证 |
保证(已有映射关系在节点增加时不会被破坏) |
一致性哈希算法通过构建一个抽象的哈希环(通常范围为 0 ~ 2³²-1),将数据和服务器节点都映射到这个环上。数据的归属由其在环上位置顺时针找到的第一个节点决定。其核心优势在于,当集群中有节点加入或退出时,只影响该节点在环上相邻小部分数据,而非全部数据重新分布,从而满足单调性。
实现上,有两个关键设计点:
虚拟节点机制:为解决实际节点过少或节点哈希值分布不均导致的负载倾斜问题,引入了虚拟节点。每个物理节点对应多个虚拟节点分布在全环上。虚拟节点数量通常为100-1000个,通过增加节点在环上的分布密度,使数据分配更均匀,负载更平衡。
高效的数据结构:由于需要频繁执行的操作是“查找大于等于给定哈希值的第一个节点”,红黑树
(std::map)因其在有序数据的插入、删除和范围查找上的高效性(时间复杂度为 O(log n)),成为实现哈希环存储的理想选择。它能够高效地支持结点频繁地增删,也具有理想的查询效率。
#include <map>
#include <string>
#include <vector>
#include <functional>
#include <algorithm>
class ConsistentHash {
private:
using RingType = std::map<uint32_t, std::string>; // 哈希环,key为虚拟节点的哈希值,value为物理节点标识
RingType ring_;
int virtualNodesPerNode_;
std::hash<std::string> stringHash;
// 优化哈希函数(FNV-1a算法)
uint32_t fnv1a_hash(const std::string& key) {
const uint32_t FNV_OFFSET_BASIS = 2166136261;
const uint32_t FNV_PRIME = 16777619;
uint32_t hash = FNV_OFFSET_BASIS;
for(char c : key) {
hash ^= static_cast<uint32_t>(c);
hash *= FNV_PRIME;
}
return hash;
}
public:
explicit ConsistentHash(int vnodes = 200) : virtualNodesPerNode_(vnodes) {}
void addNode(const std::string& node) {
for(int i = 0; i < virtualNodesPerNode_; ++i) {
// 为每个物理节点创建多个虚拟节点
std::string vnode = node + "#vn" + std::to_string(i);
uint32_t hash = fnv1a_hash(vnode);
// 处理极低概率的哈希冲突
while(ring_.count(hash)) {
hash = (hash + 1) % (1ULL << 32);
}
ring_[hash] = node; // 将虚拟节点映射到物理节点
}
}
void removeNode(const std::string& node) {
auto it = ring_.begin();
while(it != ring_.end()) {
if(it->second == node) {
it = ring_.erase(it);
} else {
++it;
}
}
}
std::string getNode(const std::string& key) {
if(ring_.empty()) return "";
uint32_t hash = fnv1a_hash(key);
auto it = ring_.lower_bound(hash); // 在红黑树中查找第一个哈希值 >= key哈希的节点
if(it == ring_.end()) {
// 若未找到,则回到环的起点(形成环形)
it = ring_.begin();
}
return it->second;
}
};
虚拟节点的生成与管理:代码中通过将物理节点标识(如IP地址)拼接索引号(如
"192.168.1.1#vn0",
"192.168.1.1#vn1")来生成虚拟节点标识。对每个虚拟节点标识计算哈希值,并将其映射到哈希环上。虚拟节点的数量对负载均衡有直接影响,更多的虚拟节点可以使负载分布更均匀,但也会增加内存开销和计算时间。
高效查找:
std::map::lower_bound:
getNode函数中的
ring_.lower_bound(hash)是关键。它利用红黑树的特性,高效地找到环上第一个哈希值大于等于数据键哈希值的虚拟节点。如果找不到(说明数据哈希值超出了当前环的最大值),则返回环的第一个节点,模拟环状结构。
哈希函数的选择:代码中使用了FNV-1a哈希算法,它是一种快速且分布性较好的非加密哈希算法,适合此类场景。也可以选择MD5等算法(取其部分输出作为长整型哈希值),因为它们能提供更好的离散性。核心是哈希函数需要具有良好的均匀性和分散性。