为什么要一致性哈希
分片存储面临同一个问题:节点数变化时,key 与节点的映射要尽量少变。用取模(hash(key) % N)最简单,但 N 从 4 改成 5 会让约八成的 key 换节点,缓存集体失效、数据大规模搬迁。一致性哈希把节点与 key 都映射到同一个整数环上,key 顺时针找到第一个节点;增删一个节点只影响相邻一段区间,其余映射不动。
基本信息
| 核心思想 | 节点与 key 同环映射,key 归属顺时针第一个节点 |
|---|---|
| 虚拟节点 | 每个物理节点在环上放多个副本点(常见 100-200),平衡数据分布 |
| 典型实现 | Redis Cluster 用 16384 个固定槽位,key 经 CRC16 取模定位槽 |
| 扩缩容影响 | 普通取模几乎全量重映射;一致性哈希只迁移相邻区间 |
| 槽位迁移 | Redis Cluster 支持槽级搬迁,迁移期间槽内 key 有重定向提示 |
典型使用场景
- 自研分片缓存或分片库:节点扩缩容时希望缓存命中率不整体塌陷
- 多级缓存或本地缓存的路由决策:按 key 固定落到同一进程,提高本地命中率
- 有状态服务分片:会话、连接与 key 绑定,节点增减时只搬迁部分分片
- 理解 Redis Cluster 的槽位与重定向机制,排查 MOVED 与 ASK 报错
- 需要把同一用户的多个 key 固定到同一节点时,用哈希标签替代改哈希函数
环与虚拟节点(简化示例)
环与虚拟节点(简化示例)
import bisect, hashlib
class Ring:
def __init__(self, nodes, vnodes=100):
self.points = [] # 环上所有虚拟节点点,按哈希值排序
for n in nodes:
for i in range(vnodes):
h = hashlib.md5(("%s#%d" % (n, i)).encode()).hexdigest()
self.points.append((int(h, 16), n))
self.points.sort()
def node_for(self, key):
h = int(hashlib.md5(key.encode()).hexdigest(), 16)
# 顺时针找到第一个虚拟节点,找不到则回到环首
i = bisect.bisect(self.points, (h,)) % len(self.points)
return self.points[i][1]
# 虚拟节点数量决定均衡度:节点少时建议 100 以上,# 节点多时可适当减少以节省内存
取模与一致性哈希对照
| 维度 | 普通取模 | 一致性哈希 |
|---|---|---|
| 节点数变化时迁移量 | 约 (N-1)/N 的 key 需要重映射 | 仅相邻区间迁移,约 1/N 左右 |
| 数据分布均衡度 | 哈希均匀时天然均衡 | 不引虚拟节点容易倾斜,靠副本点修正 |
| 实现复杂度 | 一行代码 | 需要维护环与节点上下线逻辑 |
| 典型使用者 | 分片数固定的简单场景 | Redis Cluster、Cassandra、自研分片中间件 |
Redis Cluster 固定 16384 个槽,key 用 CRC16 取模定位槽位;想让同一用户的多个 key 落在同一槽可用哈希标签(形如 user:{42}:name),槽内批量操作与事务才可能命中同一节点。
上线后更换哈希函数会让几乎所有 key 重新映射:迁移需要双读双写或先预热新位置,否则会出现大规模缓存穿透打向后端存储。