--- 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 集群方案(哈希槽机制)