一致性哈希通过虚拟节点、有序环和二分查找实现节点增减时仅迁移局部数据,避免全量重哈希;而hash(key) % node_count在节点数变化时会导致几乎所有key映射错位,引发缓存雪崩。

直接说结论:用虚拟节点 + 有序环 + 二分查找,就能让节点增减时只迁移局部数据,避免全量重哈希。
为什么不能直接用 hash(key) % node_count
这个表达式看着简单,但节点数一变(比如从 3 扩到 4),几乎所有 key 的归属都会错位。比如 hash("user_123") % 3 == 1,扩容后变成 % 4 == 2,原来存在 node1 的数据现在去 node2 查——查不到。结果就是缓存雪崩、DB 扛压、服务抖动。
一致性哈希不依赖节点总数,而是把所有节点和 key 都映射到同一个固定大小的环(通常是 0 到 2**32 - 1)上,靠顺时针“就近分配”来解耦节点数量变化。
- 节点增减只影响环上相邻一段区间,不是全局重算
- 实际部署中,单节点故障或下线,通常只有 1/N 左右的数据需要重定位(N 是节点数)
- 但纯环结构容易因节点 hash 值分布不均导致负载倾斜——这就是虚拟节点要解决的问题
ConsistentHash.get_node() 必须用二分查找,别遍历列表
环上节点位置存进 list 后如果每次 get_node 都 for 循环找第一个 ≥ key hash 的位置,时间复杂度是 O(n),节点一多(比如上百个物理机+每个配 100 个虚拟节点),延迟就明显了。
立即学习“Python免费学习笔记(深入)”;
正确做法是用 bisect.bisect_left 在已排序的 hash 值列表里二分定位:
import bisect
# self.sorted_hashes 是升序排列的节点 hash 值列表(含虚拟节点)
pos = bisect.bisect_left(self.sorted_hashes, key_hash)
if pos == len(self.sorted_hashes):
pos = 0
return self.hash_to_node[self.sorted_hashes[pos]]- 插入/删除节点时必须维持
sorted_hashes有序,推荐用sortedcontainers.SortedList或手动insort - 不要用
dict.keys()然后sorted()—— 每次调用都排序是 O(n log n),完全不可接受 - Python 标准库没有线程安全的有序集合,高并发场景下注意加锁或换用
SortedDict类型
虚拟节点数 replica_num 不是越多越好
设真实节点为 A、B、C,replica_num=3 就生成 A-0/A-1/A-2、B-0/B-1/B-2、C-0/C-1/C-2 共 9 个虚拟节点。它们在环上越分散,负载越均衡。
但要注意:
-
replica_num太小(如 1~3):节点少时仍可能聚集,倾斜明显 -
replica_num太大(如 >200):内存占用上升(每个虚拟节点存一次 hash 值 + 映射关系),ring 初始化和变更开销变高 - 生产常用值是 64、128、256;Redis Cluster 实际用的是 16384 个 slot,本质也是超细粒度虚拟节点
- MD5 输出是 128 位,转成整数再 mod
2**32时,高位信息被截断——建议用int(hashlib.md5(...).hexdigest()[:8], 16)取前 8 字符更稳
gen_key 函数必须稳定且无状态
同一个节点名(比如 "cache-01.example.com:11211")在不同机器、不同时刻调用 gen_key,必须产出完全一致的 hash 值。否则 ring 对不上,节点加入后找不到自己,或者 key 定位错乱。
- 别用
time.time()、os.getpid()、随机数参与计算 - 节点标识尽量用不变量:IP+端口、容器 hostname、K8s pod UID(非 name)
- key 字符串统一 encode 成
utf-8,避免 Python 2/3 或 locale 差异 - 测试时可打印几个典型输入的 hash 值,确认跨环境一致:
print(ConsistentHash.gen_key("node1"))
真正难的不是写完 add_node 和 get_node,而是保证所有客户端实例用同一套节点配置、同一套 hash 参数、同一版 gen_key 逻辑——配置同步失败或版本混用,环就裂开了。


















