7.8 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
2026-05-27 10:00 |
一致性哈希
概述
一致性哈希(Consistent Hashing)是分布式系统中解决数据如何均匀分配到多个节点的经典算法。它在 1997 年由 Karger 等人提出,核心目标是:当节点增减时,尽量少地迁移数据。
Redis Cluster 最终没有采用一致性哈希,而是选择了更简单的哈希槽方案,但理解一致性哈希是理解分布式分片的基础。
正文
一、传统取模方案的问题
最直观的分片方式是对 key 取模:hash(key) % N(N 是节点数)。
[!QUESTION] 这个方案有什么问题? 假设有 3 个节点,突然要加到 4 个节点——N 从 3 变成 4,几乎所有 key 的取模结果都变了,意味着几乎所有数据都需要重新分配。对于百万级数据来说,这是一场灾难。
flowchart LR
K["key: hash=7"] --> M["7 % 3 = 1<br/>Node 1"]
K2["key: hash=7"] --> M2["7 % 4 = 3<br/>Node 3"]
style M fill:#e8f5e9,stroke:#4caf50
style M2 fill:#fce4ec,stroke:#e91e63
同一个 key,只因为节点数变了,就被分配到了完全不同的节点。一致性哈希正是为了解决这个问题。
二、哈希环——一致性哈希的核心结构
一致性哈希将整个哈希值空间组织成一个虚拟的环,范围是 [0, 2^32)。
步骤:
- 节点映射:对每个节点的标识(如 IP:Port)做 hash,映射到环上的某个位置
- 数据映射:对每个 key 做 hash,同样映射到环上
- 顺时针查找:从 key 在环上的位置出发,沿顺时针方向找到的第一个节点,就是这个 key 应该存储的位置
[!TIP] 哈希函数的选择 一致性哈希要求 hash 函数的输出尽可能均匀分布,实践中常用 MurmurHash 或 xxHash 这类非加密哈希函数。它们速度快、分布性好,远优于简单的取模或 CRC32。Java 中的
TreeMap、Go 中的sort.Search都可以用来高效实现顺时针查找。
flowchart TD
N1["Node A: hash=100"] --> K1["Key 1: hash=200"]
K1 -->|"顺时针归属"| N2["Node B: hash=300"]
N2 --> K2["Key 2: hash=350"]
K2 -->|"顺时针归属"| N3["Node C: hash=500"]
N3 --> K3["Key 3: hash=80"]
K3 -->|"顺时针归属"| N1
style N1 fill:#e8eaf6,stroke:#3f51b5
style N2 fill:#e8eaf6,stroke:#3f51b5
style N3 fill:#e8eaf6,stroke:#3f51b5
style K1 fill:#fff3e0,stroke:#ff9800
style K2 fill:#fff3e0,stroke:#ff9800
style K3 fill:#fff3e0,stroke:#ff9800
上图将环形结构展开为线性示意。在实际的哈希环上,Key 3(hash=80)从 80 位置沿顺时针方向,经过 100 处的 Node A,所以它归属于 Node A。
核心查找逻辑:用一个排序数组维护环上所有节点位置,然后二分查找"第一个 >= key hash 值的位置",取模回到环头即为顺时针下一节点:
// ring 是所有节点 hash 值的升序排列
func getTargetNode(ring []uint32, keyHash uint32) uint32 {
// 二分查找:第一个 >= keyHash 的位置
idx := sort.Search(len(ring), func(i int) bool {
return ring[i] >= keyHash
})
// 如果没找到,说明 key 落在环的尾部,回到环头(顺时针绕回)
if idx == len(ring) {
idx = 0
}
return ring[idx]
}
[!TIP] 关键优势 当新增或删除一个节点时,只有该节点到逆时针方向的前一个节点之间的 key 需要迁移,其他 key 的归属完全不变。节点变动时,迁移数据量从 O(N) 降到 O(K/N)(K 是总 key 数,N 是节点数)。
三、虚拟节点——解决数据倾斜
基本的一致性哈希有一个致命问题:如果节点在环上分布不均匀,某些节点会承担远超平均值的数据量。
[!QUESTION] 极端情况 如果 3 个节点的 hash 值恰好都挤在环的某 1/4 区域,那剩余 3/4 的数据全部落到一个节点上——这个节点瞬间成为热点。
解决方案:虚拟节点(Virtual Nodes)。每个物理节点对应环上的多个虚拟节点:
flowchart TD
subgraph Ring["哈希环上的虚拟节点分布"]
VA1["Node A-1"]
VB1["Node B-1"]
VA2["Node A-2"]
VC1["Node C-1"]
VB2["Node B-2"]
VA3["Node A-3"]
VC2["Node C-2"]
VB3["Node B-3"]
VC3["Node C-3"]
end
VA1 -.->|映射| PA["物理 Node A"]
VA2 -.->|映射| PA
VA3 -.->|映射| PA
VB1 -.->|映射| PB["物理 Node B"]
VB2 -.->|映射| PB
VB3 -.->|映射| PB
VC1 -.->|映射| PC["物理 Node C"]
VC2 -.->|映射| PC
VC3 -.->|映射| PC
每个物理节点生成 100~200 个虚拟节点,分布在环的不同位置。这样即使物理节点数量很少,数据也能近似均匀分布。
type ConsistentHash struct {
replicas int // 每个物理节点的虚拟节点数
ring []uint32 // 虚拟节点 hash 排序数组
vnodeMap map[uint32]string // 虚拟节点 hash → 物理节点名
}
func (ch *ConsistentHash) Add(node string) {
for i := 0; i < ch.replicas; i++ {
// 虚拟节点 key = 物理节点名 + 编号
vkey := fmt.Sprintf("%s#%d", node, i)
h := hash(vkey)
ch.ring = append(ch.ring, h)
ch.vnodeMap[h] = node
}
sort.Slice(ch.ring, func(i, j int) bool { return ch.ring[i] < ch.ring[j] })
}
func (ch *ConsistentHash) Get(key string) string {
h := hash(key)
idx := sort.Search(len(ch.ring), func(i int) bool { return ch.ring[i] >= h })
if idx == len(ch.ring) { idx = 0 }
return ch.vnodeMap[ch.ring[idx]] // 虚拟节点 → 物理节点
}
[!NOTE] 虚拟节点数量的权衡 虚拟节点越多,数据分布越均匀,但也会增加内存开销和查找时间。经验值 100
200 个是工程上的平衡点——在 310 个物理节点的场景下已经足够均匀。当节点数增多时,虚拟节点数可以适当减少。
四、一致性哈希 vs 哈希槽
Redis Cluster 最终选择了哈希槽而非一致性哈希,两者的核心区别在于:
| 对比维度 | 一致性哈希 | 哈希槽(Redis Cluster) |
|---|---|---|
| 分片单位 | 连续的 hash 区间 | 固定的 16384 个离散槽 |
| 节点增减 | 邻居区间数据迁移 | 指定槽号迁移,粒度更可控 |
| 数据分布 | 依赖虚拟节点平衡 | 天然均匀(槽均分即可) |
| 元数据 | 只需记录节点位置 | 需要维护 slot → node 映射表 |
| 实现复杂度 | 较低 | 略高(需要槽位管理) |
[!NOTE] 为什么 Redis 选择了哈希槽? 哈希槽方案下,数据迁移的粒度是整个槽而非连续区间,运维可以精确控制"迁哪些 slot",配合
CLUSTER SETSLOT命令可以做到在线平滑迁移。而一致性哈希的迁移区间是隐式的,运维不够直观。
一致性哈希的实际应用
虽然 Redis 没有采用,但一致性哈希在分布式系统中被广泛使用:
| 系统 | 应用方式 |
|---|---|
| Amazon DynamoDB | 使用一致性哈希(带虚拟节点)管理数据分区 |
| Apache Cassandra | 早期版本使用一致性哈希,后来改为更可控的分区策略 |
| Memcached 客户端 | 客户端侧使用一致性哈希决定缓存路由 |
| Nginx + upstream | hash 负载均衡策略基于一致性哈希 |
[!QUESTION] 思考:一致性哈希适合什么场景? 当节点数量动态变化频繁、且对迁移的数据量有严格要求时(如缓存集群、CDN 节点路由),一致性哈希是更好的选择。而当节点数量相对稳定、运维需要精确控制分片时(如 Redis Cluster),哈希槽方案更为实用。
关联笔记
- hhs/Redis/07-集群方案 — Redis Cluster 集群方案(哈希槽机制)