Skip to content

[L2] 一致性哈希:虚拟节点原理与数据迁移最小化

一句话结论

一致性哈希让扩缩容时只迁移 1/N 的数据,虚拟节点解决负载不均问题。


体系讲解

1. 普通取模哈希的问题

假设有 3 个缓存节点,key 路由规则为 node = hash(key) % 3

  • 增加第 4 个节点后规则变为 % 4
  • 几乎所有 key 的路由结果都改变,导致缓存大规模失效(缓存穿透)
  • N 个节点变为 N+1 时,数据迁移量约为 N/(N+1) ≈ 100%

2. 一致性哈希的核心思路

将哈希空间构造为一个首尾相接的圆环(0 ~ 2³²−1),节点和 key 都通过哈希映射到环上:

哈希环 [0 ─────────────────────── 2³²-1]
                 ↑Node A    ↑Node B    ↑Node C
   Key1 ──→ 顺时针找到第一个节点 ──→ Node B
  • 节点加入:只影响该节点在环上的前驱节点所负责的 key,其他节点不受影响
  • 节点移除:只将该节点的 key 转移给后继节点
  • 数据迁移量:增减 1 个节点时,仅迁移约 1/N 的数据

3. 虚拟节点解决负载不均

3 个真实节点在环上的位置由哈希值决定,可能分布极度不均,导致某节点承担 80% 流量:

真实节点 A → 虚拟节点 A#1, A#2, ... A#150
真实节点 B → 虚拟节点 B#1, B#2, ... B#150
真实节点 C → 虚拟节点 C#1, C#2, ... C#150
  • 每个真实节点对应多个(通常 100~200 个)虚拟节点,均匀散布在环上
  • 节点数越多,各节点负责的 key 比例越接近 1/N
  • 扩容时新增节点的虚拟节点会从多个已有节点各取一部分 key,负载再均衡

4. 数据迁移量对比

方案增加 1 个节点的迁移量适用场景
取模哈希(N → N+1)~N/(N+1) ≈ 100%节点数固定,不扩缩容
一致性哈希(无虚拟节点)~1/N,但可能负载不均节点数稳定,数据均匀
一致性哈希(虚拟节点)~1/N,负载均匀分布式缓存/存储首选

5. 工程实践

  • Redis Cluster:使用 16384 个哈希槽(Slot)代替连续哈希环,本质是虚拟节点的有限离散化版本
  • Cassandra / DynamoDB:原生一致性哈希 + 虚拟节点
  • Nginx / 负载均衡器:一致性哈希 upstream 模块,保证同一客户端 IP/Session 路由到同一后端

考察意图

考察候选人能否说清楚一致性哈希解决的核心问题(扩缩容时数据迁移最小化),并理解虚拟节点是对"节点分布不均"问题的工程修正;同时考察能否将抽象的哈希环模型映射到 Redis Cluster 等实际中间件的实现。


追问链

  1. Redis Cluster 使用哈希槽而非一致性哈希,区别是什么?

    Redis Cluster 将 key 通过 CRC16(key) % 16384 映射到 16384 个槽,每个节点负责一段槽范围。槽的迁移更简单(只需重新分配槽区间),且客户端可以缓存槽与节点的映射表,命中时直接路由,不命中时通过 MOVED/ASK 重定向。相比真正的哈希环,槽方案更易于手动干预和可视化运维。

  2. 虚拟节点数量越多越好吗?

    不是。虚拟节点数量越多,负载越均匀,但内存开销与查找开销也线性增长(环的数据结构需要存储所有虚拟节点的位置)。通常 100~200 个虚拟节点已能让标准差降到 5% 以内,再多收益递减。

  3. 一致性哈希如何处理热点 key?

    一致性哈希本身不解决热点问题——热点 key 永远路由到同一节点,虚拟节点也无法分散单个 key 的流量。需要额外手段:①本地内存缓存(JVM/PHP OPcache 级别);②key 加随机后缀打散到多个副本(hot_key_0hot_key_N),读时随机选取副本;③CDN 或专用缓存层。


易错点

  1. 认为一致性哈希完全消除数据迁移:一致性哈希只是最小化迁移量(约 1/N),节点增减时仍有数据需要搬移,只是迁移范围局限于相邻节点之间。

  2. 忽略虚拟节点的内存开销:线上几百个节点 × 每节点 200 个虚拟节点 = 数万条记录,需要合理设置虚拟节点数量,避免路由表过大影响查找性能。

  3. 混淆一致性哈希的"一致性"与 CAP 的"一致性":一致性哈希的"一致"指扩缩容前后路由结果尽量不变(路由稳定性),与 CAP 中数据的强一致性完全无关。


代码示例

php
// 一致性哈希环(含虚拟节点)简化实现
class ConsistentHashRing
{
    private array $ring  = [];  // [virtualNodeHash => realNodeName]
    private int $replicas;      // 每个真实节点的虚拟节点数

    public function __construct(int $replicas = 150)
    {
        $this->replicas = $replicas;
    }

    public function addNode(string $node): void
    {
        for ($i = 0; $i < $this->replicas; $i++) {
            $hash              = crc32("{$node}#{$i}");
            $this->ring[$hash] = $node;
        }
        ksort($this->ring);
    }

    public function removeNode(string $node): void
    {
        for ($i = 0; $i < $this->replicas; $i++) {
            unset($this->ring[crc32("{$node}#{$i}")]);
        }
    }

    public function getNode(string $key): ?string
    {
        if (empty($this->ring)) {
            return null;
        }
        $hash = crc32($key);
        foreach ($this->ring as $nodeHash => $node) {
            if ($hash <= $nodeHash) {
                return $node; // 顺时针找到第一个节点
            }
        }
        return reset($this->ring); // 绕过环尾,回到环头
    }
}

$ring = new ConsistentHashRing(150);
$ring->addNode('cache-1');
$ring->addNode('cache-2');
$ring->addNode('cache-3');

echo $ring->getNode('user:1001'); // 稳定路由到同一节点

基于 Apache License 2.0 开源