---
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 的取模结果都变了,意味着**几乎所有数据都需要重新分配**。对于百万级数据来说,这是一场灾难。
```mermaid
flowchart LR
K["key: hash=7"] --> M["7 % 3 = 1
Node 1"]
K2["key: hash=7"] --> M2["7 % 4 = 3
Node 3"]
style M fill:#e8f5e9,stroke:#4caf50
style M2 fill:#fce4ec,stroke:#e91e63
```
同一个 key,只因为节点数变了,就被分配到了完全不同的节点。一致性哈希正是为了解决这个问题。
## 二、哈希环——一致性哈希的核心结构
一致性哈希将整个哈希值空间组织成一个**虚拟的环**,范围是 `[0, 2^32)`。
**步骤:**
1. **节点映射**:对每个节点的标识(如 IP:Port)做 hash,映射到环上的某个位置
2. **数据映射**:对每个 key 做 hash,同样映射到环上
3. **顺时针查找**:从 key 在环上的位置出发,沿顺时针方向找到的**第一个节点**,就是这个 key 应该存储的位置
> [!TIP] 哈希函数的选择
> 一致性哈希要求 hash 函数的输出尽可能均匀分布,实践中常用 **MurmurHash** 或 **xxHash** 这类非加密哈希函数。它们速度快、分布性好,远优于简单的取模或 CRC32。Java 中的 `TreeMap`、Go 中的 `sort.Search` 都可以用来高效实现顺时针查找。
```mermaid
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 值的位置",取模回到环头即为顺时针下一节点:
```go
// 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)**。每个物理节点对应环上的多个虚拟节点:
```mermaid
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 个虚拟节点,分布在环的不同位置。这样即使物理节点数量很少,数据也能近似均匀分布。
```go
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 个是工程上的平衡点——在 3~10 个物理节点的场景下已经足够均匀。当节点数增多时,虚拟节点数可以适当减少。
## 四、一致性哈希 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 集群方案(哈希槽机制)