直接用 std::hash 不行,因其无法感知节点变化,导致增减节点时所有键重映射;一致性哈希需将节点和键映射至同一逻辑环,通过虚拟节点均匀分布,并用 std::map 维护有序哈希环,配合 upper_bound 与回绕实现顺时针查找,且须统一哈希种子与虚拟节点数。

为什么直接用 std::hash 不行?
一致性哈希的核心不是“算得快”,而是“节点增减时,尽可能少地迁移键”。std::hash 对同一输入稳定,但对节点变化完全无感知——加一台机器,所有 key % node_count 都得重算,违背设计初衷。必须把节点映射到一个逻辑环上,再让 key 落在顺时针最近的节点。虚拟节点(virtual node)就是解决真实节点在环上分布不均的问题:每个物理节点对应多个哈希值(比如 100 个),分散插进环里,降低偏斜。
怎么构造带虚拟节点的哈希环?
用 std::map<uint64_t std::string></uint64_t> 最直接:key 是虚拟节点哈希值(uint64_t 足够覆盖 MD5 前 8 字节或 MurmurHash3 输出),value 是对应物理节点标识(如 "192.168.1.10:8080")。插入时循环生成虚拟节点哈希:
for (int i = 0; i < virtual_replicas_; ++i) {
std::string node_key = node + ":" + std::to_string(i);
uint64_t hash = murmur3_64(node_key.data(), node_key.size());
ring_[hash] = node;
}
注意三点:
• 不能用 std::unordered_map,因为需要 upper_bound 找顺时针第一个节点;
• 哈希函数必须是确定性的、分布均匀的,别手写 std::hash<string>()</string> 拼接,它不保证跨平台一致;
• 虚拟节点数建议设为 100–200,太少起不到均衡作用,太多增加内存和查找开销(map::upper_bound 是 O(log N),N 是总虚拟节点数)。
如何定位 key 对应的节点?
对 key 做同样哈希(用同一个函数,比如 murmur3_64),然后在 ring_ 中找第一个 ≥ 该哈希值的节点:
立即学习“C++免费学习笔记(深入)”;
uint64_t hash = murmur3_64(key.data(), key.size());
auto it = ring_.upper_bound(hash);
if (it == ring_.end()) {
it = ring_.begin(); // 环形回绕
}
return it->second;
常见错误:
• 忘记回绕,导致末尾 key 全部丢失;
• 对 key 和 node 用了不同哈希函数,环和 key 不在同一个空间;
• 把 lower_bound 当成顺时针查找——它返回 ≥ 的第一个,但若 key 哈希恰好命中某个虚拟节点,lower_bound 会停在那里,而 upper_bound 能确保“严格大于”,配合回绕才符合经典定义。
节点增删后怎么保持服务可用?
增删节点只需操作 ring_:添加时插入全部虚拟节点,删除时遍历 erase 所有以该 node 为 value 的条目(map 不支持按 value 删除,得先缓存 key 集合或改用双向索引)。关键点在于:
• 不要清空重建整个 ring,否则瞬时所有 key 迁移;
• 删除节点前,最好通知上游逐步切流,避免正在处理的请求失败;
• 虚拟节点数必须全局统一,否则新老节点虚拟副本数量不等,负载会倾斜——这个配置一旦上线就不能轻易改。
最易被忽略的是哈希函数的种子(seed):MurmurHash3 等允许传入 seed,如果不同进程/机器用了不同 seed,同一个 node 字符串生成的虚拟节点位置就不同,整个环就错乱了。务必硬编码 seed,比如 0xdeadbeef。


















