专栏文章
专栏文章
网络算法系列
1. 网络算法 #01:轮询与加权轮询 2. 网络算法 #02:最小连接数 3. 网络算法 #03:Jump Consistent Hash 4. 网络算法 #04:Rendezvous Hashing 5. 网络算法 #05:TCP 拥塞控制(CUBIC 与 BBR) 6. 网络算法 #06:QUIC 丢包恢复与拥塞控制 7. 网络算法 #07:经典一致性哈希(虚拟节点) 8. 网络算法 #08:P2C(Power of Two Choices) 9. 网络算法 #09:TCP 滑动窗口与流量控制 10. 网络算法 #10:Maglev Hashing

网络算法 #03:Jump Consistent Hash

发布于 2026-06-04 04:39 👁 10 次阅读

Jump Consistent Hash 用 5 行代码实现 O(1) 空间、O(ln N) 时间的一致性哈希,扩容时只迁移 1/(N+1) 的 key,被 Google 分布式存储和负载均衡系统广泛采用。

相关文章轮询与加权轮询 · 最小连接数 · Rendezvous Hashing

jump consistent hash

目录

章节 说明
问题背景 取模哈希与一致性哈希的不足,Jump Hash 的定位
核心算法 5 行代码实现,字段定义与伪代码
算法原理推导 概率分析与线性同余生成器的选用
执行追踪 key=12345678,num_buckets=5 的逐步演示
迁移代价证明 为何恰好只有 1/(N+1) 的 key 迁移
异常与边界场景 num_buckets=0/1、key 分布不均等 3 个场景
与一致性哈希对比 空间、时间、灵活性三维度对比
实际应用 Google Guice、分布式存储、CDN 节点选择
参考资料 论文与文档

问题背景

取模哈希的缺陷

最简单的分片方案是取模哈希:bucket = key % N。当桶数量从 N 变为 N+1 时,几乎所有 key 的映射结果都会发生变化,导致大规模数据迁移。

示例:10 个 key,桶数从 3 扩到 4:

key=0: 0%3=0 → 0%4=0  (不变)
key=1: 1%3=1 → 1%4=1  (不变)
key=2: 2%3=2 → 2%4=2  (不变)
key=3: 3%3=0 → 3%4=3  (迁移)
key=4: 4%3=1 → 4%4=0  (迁移)
key=5: 5%3=2 → 5%4=1  (迁移)
key=6: 6%3=0 → 6%4=2  (迁移)
key=7: 7%3=1 → 7%4=3  (迁移)
...

约 75% 的 key 需要迁移,对生产系统是灾难性的。

一致性哈希的不足

传统一致性哈希(Karger et al. 1997)通过虚拟节点环解决了大规模迁移问题,但:

Jump Consistent Hash 的定位

Google 2014 年发表的论文提出 Jump Consistent Hash,目标是:


核心算法

核心数据结构

算法只需两个局部变量,无任何外部状态:

变量 类型 含义
key uint64_t 输入 key,同时用作线性同余生成器的状态,在迭代中被原地更新
num_buckets int32_t 目标桶的总数量,取值范围 [1, INT32_MAX]
b int64_t 当前已确认的桶号,初始为 -1
j int64_t 候选桶号(跳跃目标),初始为 0

完整伪代码

int32_t JumpConsistentHash(uint64_t key, int32_t num_buckets) {
    int64_t b = -1, j = 0;
    while (j < num_buckets) {
        b = j;
        key = key * 2862933555777941757ULL + 1;
        j = (b + 1) * (double(1LL << 31) / double((key >> 33) + 1));
    }
    return b;
}

关键常数说明

常数 含义
2862933555777941757ULL 0x27BB2EE687B0B0FDULL 线性同余生成器的乘法因子,由 Knuth 推荐,具有良好的随机性
1LL << 31 2147483648 2^31,用于将 64 位随机数归一化到 (0, 1] 范围
key >> 33 取 key 的高 31 位,避免低位随机性不足

核心公式解读

j = (b + 1) * (2^31 / ((key >> 33) + 1))

r = (key >> 33 + 1) / 2^31,则 r 是 (0, 1] 之间的伪随机数,公式等价于:

j = floor((b + 1) / r)

这意味着:以概率 1/(b+2) 跳到桶 b+1,以更小概率跳到更远的桶。


算法原理推导

核心问题

给定 key,桶数从 N 扩到 N+1 时,哪些 key 应该跳到新桶 N?

答案:恰好 1/(N+1) 比例的 key 应该跳到新桶,且每个 key 跳到新桶的概率相等(均为 1/(N+1))。这是负载均衡的最优解。

概率递推

ch(key, n) 为 key 在 n 个桶时的分配桶号。当桶数从 n 扩到 n+1 时:

因此,ch(key, n+1) 的值:

从递推到迭代

直接递推需要 O(N) 步。关键优化是跳跃:对于给定 key,在某个桶号 b 处,直接计算下一次「发生跳跃」的桶号 j

设当前在桶 b,下一次跳跃发生在桶 j(即 key 在 b+1..j-1 这些桶都「选择留下」,在桶 j 选择「跳到新桶」)。

j 的期望公式:给定伪随机数 r ∈ (0,1],令 j = floor((b+1)/r),使得 j > b 且跳跃位置服从正确的概率分布。

线性同余生成器

每次迭代用 key = key * M + 1 更新 key 作为伪随机源,保证:


执行追踪

输入key = 12345678num_buckets = 5

初始状态b = -1j = 0


迭代 1(j=0 < 5,继续):

b = 0
key = 12345678 * 2862933555777941757 + 1
    = 12345678 * 0x27BB2EE687B0B0FD + 1
    = 9647097690860584595  (mod 2^64)
key >> 33 = 9647097690860584595 >> 33 = 1125557...  ≈ 1125557899
r = (1125557899 + 1) / 2^31 ≈ 0.5239
j = floor((0 + 1) / 0.5239) = floor(1.909) = 1

状态:b=0, j=1


迭代 2(j=1 < 5,继续):

b = 1
key = 9647097690860584595 * 2862933555777941757 + 1
    = 3421679987654321001  (mod 2^64,示意值)
key >> 33 ≈ 399123456
r = (399123456 + 1) / 2^31 ≈ 0.1858
j = floor((1 + 1) / 0.1858) = floor(10.76) = 10

状态:b=1, j=10


迭代 3(j=10 >= 5,退出循环):

while (10 < 5) → false,退出
return b = 1

结果:key=12345678 被分配到桶 1


追踪说明:算法在 j 第一次超出 num_buckets 时停止,b 保存的是最后一次合法的桶号。迭代次数为 O(ln N):5 个桶约迭代 2-3 次,1000 个桶约迭代 7 次。


迁移代价证明

结论

当桶数从 N 扩到 N+1 时,恰好有 1/(N+1) 比例的 key 迁移到新桶 N,其余 N/(N+1) 的 key 保持不变。

直觉证明

设有 K 个 key 均匀分布在 N 个桶中,每个桶平均有 K/N 个 key。

扩容后有 N+1 个桶,为了保持均匀,每个桶平均有 K/(N+1) 个 key。

新桶 N 需要接收 K/(N+1) 个 key,占总量的 1/(N+1)。

这 K/(N+1) 个 key 来自原来的 N 个桶,每个桶贡献 (K/N) - (K/(N+1)) = K/(N(N+1)) 个 key,即每个桶迁移比例为:

(K/(N(N+1))) / (K/N) = 1/(N+1)

每个桶迁移相同比例,负载均衡最优。

算法实现的保证

Jump Consistent Hash 通过线性同余生成器产生的伪随机序列,保证对于任意 key:

这个概率由 j = floor((b+1)/r) 公式精确控制。


异常与边界场景

场景 1:num_buckets = 0

JumpConsistentHash(key, 0)

分析:初始 j = 0,循环条件 j < 0 为 false,直接退出。返回 b = -1

处理:调用方必须检查返回值是否为 -1,并抛出异常或返回错误。不能直接将 -1 作为桶号使用。

int bucket = JumpConsistentHash(key, numBuckets);
if (bucket < 0) throw new IllegalArgumentException("num_buckets must be >= 1");

场景 2:num_buckets = 1

JumpConsistentHash(key, 1)

分析:初始 j = 0,进入循环,b = 0,然后计算新 j。由于任何正数 floor((0+1)/r) >= 1,新 j >= 1,循环退出。

结果:所有 key 都返回 0,符合预期——只有一个桶时所有请求都分配到同一桶。

场景 3:key 分布不均匀(哈希质量差)

问题:Jump Consistent Hash 假设输入 key 的低位具有足够随机性。如果 key 本身分布不均(如全为偶数、递增整数),可能导致负载不均。

分析:算法内部使用 key >> 33(高位),但初始 key 的低位偏斜仍会通过线性同余传播到后续迭代,影响均匀性。

解决:在调用前先对 key 做一次哈希预处理:

// 使用 MurmurHash3 或 xxHash 预处理 key
long hashedKey = MurmurHash3.hash64(originalKey);
int bucket = JumpConsistentHash(hashedKey, numBuckets);

场景 4:节点缩容(不连续编号)

问题:Jump Consistent Hash 要求桶编号连续(0..N-1)。如果删除的不是最后一个节点(如删除中间节点 5),节点编号出现空洞,算法无法直接处理。

解决方案

  1. 重映射表:维护逻辑桶号到物理节点的映射,缩容时只删除最后一个逻辑桶并更新映射表。
  2. 只缩减末尾节点:业务上约定只允许删除编号最大的节点,维持连续性。
  3. 切换算法:需要频繁不规则增删节点时,改用传统一致性哈希。

与一致性哈希对比

维度 Jump Consistent Hash 传统一致性哈希
空间复杂度 O(1),无需存储结构 O(N) 虚拟节点
查找时间 O(ln N) 迭代 O(log N) 二分搜索
迁移代价 最优 1/(N+1) 接近最优(虚拟节点越多越均匀)
代码复杂度 5 行 数百行(哈希环 + 虚拟节点)
节点任意删除 不支持(只能删末尾节点) 支持任意节点增删
节点任意添加 支持(追加到末尾) 支持任意位置添加
负载均匀性 理论最优 取决于虚拟节点数量
适用场景 节点数量稳定增长 节点频繁任意增删

选择建议


实际应用

Google 内部负载均衡

Jump Consistent Hash 出自 Google 论文,在 Google 内部用于:

分布式存储分片

适用于分片数量单调递增的存储系统:

// 示例:按 userId 路由到存储分片
public int getShardId(long userId, int totalShards) {
    return JumpConsistentHash.hash(userId, totalShards);
}

扩容时(totalShards 从 N 增到 N+1),只有 1/(N+1) 的用户数据需要从旧分片迁移到新分片,迁移脚本逻辑简单且数据量可控。

CDN 节点选择

对于内容 CDN,将内容 ID 哈希到边缘节点:

node = JumpConsistentHash(contentId, edgeNodeCount)

新增边缘节点时,只有少量内容需要从其他节点迁移,大部分内容缓存仍然有效(缓存命中率不会骤降)。

数据库水平分表

-- 逻辑:将 user_id 映射到分表编号
table_index = JumpConsistentHash(user_id, table_count)
-- 查询表:user_{table_index}

扩表时(table_count++),只有 1/(table_count) 的数据需要迁移,可以在业务低峰期逐步完成。


参考资料

  1. 原始论文:Lamping, J., & Veach, E. (2014). A Fast, Minimal Memory, Consistent Hash Algorithm. arXiv:1406.2294. https://arxiv.org/abs/1406.2294
  2. Knuth 乘法因子:Knuth, D. E. (1997). The Art of Computer Programming, Volume 2: Seminumerical Algorithms (3rd ed.). Section 3.2.1, Linear Congruential Method.
  3. 一致性哈希原始论文:Karger, D., Lehman, E., Leighton, T., Panigrahy, R., Levine, M., & Lewin, D. (1997). Consistent Hashing and Random Trees. STOC 1997.
  4. Java 实现参考:Google Guava Hashing.consistentHash(). https://guava.dev/releases/snapshot/api/docs/com/google/common/hash/Hashing.html#consistentHash(com.google.common.hash.HashCode,int)
  5. Go 实现stathat/consistentdgryski/go-jump. https://github.com/dgryski/go-jump
← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论