分布式系统处理海量数据时,必须通过分片 (Sharding) 将数据分散到多个节点。一致性哈希解决了传统 Hash 取模在节点增减时的大量数据迁移问题。
1. 传统 Hash 分片的问题
传统方式: node = hash(key) % N
N=3 时:
hash(key) % 3 = 0 → Node0
hash(key) % 3 = 1 → Node1
hash(key) % 3 = 2 → Node2
新增 Node3 (N=4):
原来 75% 的数据需要重新映射!
hash(key) % 4 和 hash(key) % 3 结果不同的概率 = 3/4
问题:节点数量变化时,几乎所有数据都需要迁移,成本极高。
2. 一致性哈希 (Consistent Hashing)
1997 年由 MIT 的 David Karger 等人提出,核心思想是将节点和数据映射到同一个哈希环上。
2.1 算法原理
哈希环 [0, 2^32-1]:
Node A (hash=10)
●
/ \
●----/ \----●
Node D Node B
(hash=95) (hash=50)
\----\ /----/
\ /
●
Node C (hash=80)
数据定位: 从数据 hash 值顺时针找到第一个节点
hash(key1)=25 → Node B
hash(key2)=60 → Node C
hash(key3)=90 → Node D
2.2 节点增减的影响
新增 Node E (hash=30):
只有 Node A→Node E 之间的数据从 Node B 迁移到 Node E
迁移比例 ≈ 1/N(均匀分布假设)
删除 Node B:
Node B 上的数据顺时针迁移到 Node C
仅影响 Node B 的数据
2.3 虚拟节点 (Virtual Nodes)
解决数据倾斜问题:一个物理节点对应多个虚拟节点。
物理节点 A → 虚拟节点 A1(10), A2(110), A3(210)
物理节点 B → 虚拟节点 B1(50), B2(150), B3(250)
数据更均匀分布,物理节点负载趋于平衡
2.4 Python 实现
import hashlib
import bisect
class ConsistentHashRing:
def __init__(self, replicas=150):
self.replicas = replicas # 每个物理节点的虚拟节点数
self.ring = {} # {hash: node}
self.sorted_keys = [] # 排序的哈希值
self.nodes = set()
def _hash(self, key):
return int(hashlib.md5(key.encode('utf-8')).hexdigest(), 16)
def add_node(self, node):
self.nodes.add(node)
for i in range(self.replicas):
key = self._hash(f"{node}:{i}")
self.ring[key] = node
bisect.insort(self.sorted_keys, key)
def remove_node(self, node):
self.nodes.discard(node)
for i in range(self.replicas):
key = self._hash(f"{node}:{i}")
del self.ring[key]
idx = bisect.bisect_left(self.sorted_keys, key)
self.sorted_keys.pop(idx)
def get_node(self, key):
if not self.ring:
return None
h = self._hash(key)
idx = bisect.bisect_right(self.sorted_keys, h) % len(self.sorted_keys)
return self.ring[self.sorted_keys[idx]]
def get_nodes(self, key, n=3):
"""获取顺时针的 n 个节点(用于多副本)"""
if not self.ring or n > len(self.nodes):
return list(self.nodes)
h = self._hash(key)
idx = bisect.bisect_right(self.sorted_keys, h)
result = []
seen = set()
while len(result) < n:
node = self.ring[self.sorted_keys[idx % len(self.sorted_keys)]]
if node not in seen:
seen.add(node)
result.append(node)
idx += 1
return result
# 使用
ch = ConsistentHashRing(replicas=150)
ch.add_node("192.168.1.1")
ch.add_node("192.168.1.2")
ch.add_node("192.168.1.3")
node = ch.get_node("user:12345")
replicas = ch.get_nodes("order:67890", n=3) # 3 个副本节点
3. 分片策略对比
3.1 Hash 分片
Shard = hash(key) % N
| 优点 | 缺点 |
|---|---|
| 数据分布均匀 | 范围查询需扫描所有分片 |
| 写入分散,无热点 | 新增分片需大量迁移 |
| 适合点查 | 无法按时间序分区 |
3.2 Range 分片
Shard 0: user_id [1, 1000000)
Shard 1: user_id [1000000, 2000000)
Shard 2: user_id [2000000, 3000000)
或按时间:
Shard 0: 2024-01 ~ 2024-03
Shard 1: 2024-04 ~ 2024-06
| 优点 | 缺点 |
|---|---|
| 范围查询高效 | 可能产生热点(新数据集中在最新分片) |
| 扩展简单(追加新范围) | 数据分布可能不均匀 |
| 适合时间序列数据 | 需预分片或动态分裂 |
3.3 混合分片 (Hash + Range)
先按时间 Range 分表,再按 UserID Hash 分库
2024_01_order_0 (user_id % 4 = 0)
2024_01_order_1 (user_id % 4 = 1)
...
3.4 目录分片 (Directory Based)
维护一个元数据服务记录 key→shard 的映射,灵活性最高。
# 元数据表
shard_map = {
'user:1': 'shard_0',
'user:2': 'shard_1',
# ...
}
# 查找
shard = shard_map.get('user:123') or compute_shard('user:123')
4. 水平扩展与数据迁移
4.1 双倍扩展法
N=2 → N=4:
Shard_0 → Shard_0, Shard_2
Shard_1 → Shard_1, Shard_3
只需将原来 shard 的数据 COPY 拆分,无需重新 hash
4.2 一致性哈希的渐进迁移
def migrate_data(consistent_hash, new_node):
"""迁移应落在 new_node 上的数据"""
# 1. 将 new_node 加入环(双写阶段)
consistent_hash.add_node(new_node)
# 2. 遍历数据,将归属变为 new_node 的数据复制过去
for key, value in source_db.scan():
assigned = consistent_hash.get_node(key)
if assigned == new_node:
new_node.store(key, value)
# 3. 验证一致性后,确认新节点服务读请求
# 4. 删除旧节点上的冗余数据
4.3 在线扩缩容流程
1. 新增节点加入集群(不分配流量)
2. 后台迁移数据到新节点(双写:旧节点仍写入)
3. 数据迁移完成,新节点加入读流量(逐步放量)
4. 监控无误后,旧节点停止写入,清理冗余数据
5. 缩容则反向执行
5. 实战:Redis Cluster 的分片
Redis Cluster 使用 哈希槽 (Hash Slot):16384 个槽均匀分配到各节点。
slot = CRC16(key) % 16384
Node A: slots 0-5460
Node B: slots 5461-10922
Node C: slots 10923-16383
# 多 key 操作需在同一个 slot:用 hash tag
{user:123}:profile → 只 hash "user:123"
{user:123}:orders → 同一 slot
# 查看集群槽分配
redis-cli -c -p 7000 cluster slots
# 迁移槽
redis-cli --cluster reshard 127.0.0.1:7000 \
--cluster-from node-id-a \
--cluster-to node-id-b \
--cluster-slots 1000
6. 实战:Cassandra 一致性哈希
Cassandra 使用 一致性哈希 + 虚拟节点 (vnodes):
默认 256 tokens/节点
Token = hash(partition_key)
数据属于顺时针方向的 N 个 replica 节点
复制因子 RF=3:
Primary: 负责该 token 区间的节点
Replica1: 顺时针下一个节点
Replica2: 顺时针再下一个节点
-- 创建 keyspace 指定复制策略
CREATE KEYSPACE mykeyspace
WITH replication = {
'class': 'NetworkTopologyStrategy',
'datacenter1': 3
};
总结
| 场景 | 推荐分片策略 |
|---|---|
| 用户数据(点查为主) | Hash 分片 + 一致性哈希 |
| 日志/时序数据 | Range 分片(时间) |
| 需要灵活调度 | 目录分片 |
| 缓存层 | 一致性哈希(Redis/Memcached) |
| 数据库中间件 | 范围分片 + 自动分裂 |
水平扩展关键:
- 预分片:提前分成足够多的虚拟分片,减少后续迁移
- 渐进迁移:避免全量迁移,逐步切换流量
- 双写机制:迁移期间同时写入新旧节点
- 元数据服务:记录分片映射,支持灵活调度
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。