一致性哈希 Consistent Hashing

创建时间:2026-05-21
分类:分布式系统哈希算法负载均衡
难度:中等

一句话理解

一致性哈希是一种特殊的哈希算法:当集群中增加或减少节点时,它能保证大部分数据不需要重新分配,只有少部分数据受影响。

类比:你有 3 个书架放书,普通哈希像「打乱重排」,一致性哈希像「只挪动几本书」。

为什么需要一致性哈希?

场景:分布式缓存

假设你有 3 台缓存服务器,需要把数据均匀分布到它们上面。最直觉的做法:

server = hash(key) % N  (N = 服务器数量)

问题在哪?

场景 A:1 台服务器宕机

N 从 3 变成 2,几乎所有 key 的 hash % N 结果都变了!

~66% 的数据需要重新分配

场景 B:新增 1 台服务器

N 从 3 变成 4,同样几乎所有 key 的映射都变了。

~75% 的数据需要重新分配

核心矛盾:传统 hash % N 的方式,N 一旦变化,所有数据的归属都会打乱重来。这在大规模系统中会导致:

一致性哈希的核心思想

一致性哈希的巧妙之处:它把哈希值空间组织成一个环(Hash Ring)

0 / 232
230
231
231 + 230
A
B
C
K1
K2
K3
工作原理

哈希环上的每个位置对应一个哈希值(0 到 232-1)。

三步理解一致性哈希

  1. 构建哈希环
    把 0 ~ 232-1 的整数空间首尾相连,形成一个环。每台服务器根据 hash(IP) 映射到环上的一个点。
  2. 数据映射
    对每个数据 key 计算 hash(key),在环上找到对应位置,然后沿顺时针方向找到第一台服务器,该数据就存在这台服务器上。
  3. 节点增减时只影响局部
    新增节点:只接管它逆时针方向相邻节点的一部分数据。
    删除节点:它负责的数据顺时针移交给下一个节点。
    其他节点完全不受影响!

图解:节点增减的影响

增加节点 D

D 落在 A 和 B 之间:

只迁移 ~25% 的数据

删除节点 B

B 下线后:

只迁移 ~33% 的数据

对比传统方式:传统 hash % N 增减节点时,几乎所有数据(~75%~100%)都要迁移。一致性哈希只影响 1/N 的数据量。

虚拟节点:解决分布不均

问题:数据倾斜

如果只有 3 台服务器,它们在环上的位置可能分布不均匀,导致某台服务器承担过多数据。

解决方案:虚拟节点(Virtual Nodes)

为每台服务器创建多个「分身」,每个分身映射到环上的不同位置:

虚拟节点越多,数据分布越均匀(大数定律)。实际生产中通常用 100~200 个虚拟节点

0
230
231
231+230
A
B
C
A#2
A#3
B#2
B#3
C#2
C#3

Python 实现

import hashlib
from bisect import bisect_right

class ConsistentHash:
    def __init__(self, nodes=None, replicas=150):
        # replicas: 每个真实节点的虚拟节点数量
        self.replicas = replicas
        self.ring = {}        # hash值 -> 真实节点名
        self.sorted_keys = [] # 排序后的hash值列表
        for node in (nodes or []):
            self.add_node(node)

    def _hash(self, key):
        # 用 MD5 生成 32 位整数哈希值
        return int(hashlib.md5(key.encode()).hexdigest(), 16)

    def add_node(self, node):
        # 为一个真实节点创建多个虚拟节点
        for i in range(self.replicas):
            vnode_key = f"{node}:v{i}"
            h = self._hash(vnode_key)
            self.ring[h] = node
            self.sorted_keys.append(h)
        self.sorted_keys.sort()

    def remove_node(self, node):
        # 移除一个节点及其所有虚拟节点
        for i in range(self.replicas):
            vnode_key = f"{node}:v{i}"
            h = self._hash(vnode_key)
            del self.ring[h]
            self.sorted_keys.remove(h)

    def get_node(self, key):
        # 查找 key 应该存储在哪个节点
        if not self.ring:
            return None
        h = self._hash(key)
        # 二分查找:找到顺时针方向第一个 >= h 的位置
        idx = bisect_right(self.sorted_keys, h)
        # 如果超出列表末尾,绕回环的起点
        if idx == len(self.sorted_keys):
            idx = 0
        return self.ring[self.sorted_keys[idx]]


# ---- 使用示例 ----
ch = ConsistentHash(["Server-A", "Server-B", "Server-C"])

# 查找数据归属
keys = ["user:1001", "user:1002", "order:500", "product:88"]
for k in keys:
    print(f"{k} -> {ch.get_node(k)}")

# 新增节点后,大部分 key 不变
ch.add_node("Server-D")
print("--- 新增 Server-D 后 ---")
for k in keys:
    print(f"{k} -> {ch.get_node(k)}")

对比总结

对比维度 传统 hash % N 一致性哈希
节点增减影响 几乎所有数据重新分配 仅 1/N 数据受影响
缓存命中率 节点变动时急剧下降 基本保持稳定
数据分布均匀性 天然均匀 需要虚拟节点保证
实现复杂度 极简(一行代码) 中等(需要维护环和二分查找)
适用场景 固定节点数、无扩缩容 分布式缓存、弹性伸缩

实际应用场景

🗄️
分布式缓存
Memcached、Redis Cluster 用一致性哈希分片
🌐
CDN 路由
将用户请求路由到最近的 CDN 节点
📦
分布式数据库
Cassandra、DynamoDB 的数据分区策略
🔀
负载均衡
Nginx 的 ketama 一致性哈希负载均衡
📨
消息队列
Kafka 分区分配、RabbitMQ 集群
🔗
微服务注册
服务发现时的请求粘滞(Sticky Session)

延伸阅读

论文出处:一致性哈希最早由 David Karger 等人在 1997 年的论文 "Consistent Hashing and Random Trees" 中提出。

关联笔记