一致性 Hash(Consistent Hashing)详解

一致性 Hash 是分布式缓存系统中解决「数据如何均匀分布 + 节点变化时如何减少数据迁移」问题的一种算法

它最经典的应用场景:

  • Redis 集群数据分片

  • Memcached 分布式缓存

  • CDN 节点调度

  • 分布式数据库分片

  • 服务节点负载均衡


1. 为什么需要一致性 Hash?

先看传统 Hash 分片。

假设我们有 3 台缓存服务器:

Cache-A
Cache-B
Cache-C

现在有数据:

user:1001
user:1002
user:1003
...

我们希望把数据均匀放到不同机器。

最简单方法:

服务器编号 = hash(key) % 节点数量

例如:

node = hash("user:1001") % 3

结果:

hash(user:1001)=100

100 % 3 = 1

放到 Cache-B

结构:

             hash(key)%3

user:1001  ----------> Cache-B
user:1002  ----------> Cache-A
user:1003  ----------> Cache-C

看起来很好。


2. 问题在哪里?

假设业务增长,需要增加一台机器:

之前:

3个节点

Cache-A
Cache-B
Cache-C

变成:

4个节点

Cache-A
Cache-B
Cache-C
Cache-D

计算方式变了:

以前:

hash(key)%3

现在:

hash(key)%4

大量数据映射都会变化。

例如:

key 原节点 新节点
user:1 A C
user:2 B D
user:3 C A
user:4 A B

结果:

原来的缓存:

        100万个key

A ---------------- 33万
B ---------------- 33万
C ---------------- 34万


增加D之后:

A ---------------- 25万
B ---------------- 25万
C ---------------- 25万
D ---------------- 25万

大量 key 迁移。

造成:

1. 缓存雪崩

大量请求同时访问数据库:

缓存:

user:1  miss
user:2  miss
user:3  miss
...

        ↓

数据库压力暴增

2. 网络迁移压力

需要搬:

Cache-A
   |
   | 大量数据复制
   ↓
Cache-D

所以:

普通 Hash 最大的问题:节点数量变化导致几乎所有 key 重新分布。


3. 一致性 Hash 的核心思想

一致性 Hash 不再:

hash(key) % 节点数量

而是:

把整个 Hash 空间组织成一个环。

例如:

假设 Hash 值范围:

0 ~ 999

首尾连接:

              0
          /       \
       900         100


    800             200


       700       300
          \     /
             500

这叫:

Hash Ring(哈希环)


4. 节点如何放入 Hash 环?

服务器也进行 Hash:

例如:

hash(Cache-A)=100

hash(Cache-B)=400

hash(Cache-C)=700

放入环:

                 0


        Cache-A
           |
100 ----------------


                 

400
 |
Cache-B



700
 |
Cache-C

5. 数据如何寻找节点?

数据 key 也 Hash:

例如:

hash(user:1001)=250

放到环:

                 0


        Cache-A
           |
100


250  <---- user:1001


400
 |
Cache-B


700
 |
Cache-C

规则:

顺时针找到第一个节点。

所以:

user:1001

250

顺时针

↓

Cache-B

存储:

user:1001

       ↓

Cache-B

6. 一致性 Hash 最大优势

增加节点

现在增加:

Cache-D

计算:

hash(Cache-D)=550

加入:

100 Cache-A

400 Cache-B

550 Cache-D

700 Cache-C

以前:

400 ~ 700

全部属于 Cache-C

现在:

400 ~ 550

属于 Cache-D

只有这一部分数据变化:

原 Cache-C:

400~700


变成:


400~550  Cache-D

550~700  Cache-C

也就是说:

新增一个节点:

只影响环上的一小部分数据。


7. 删除节点怎么办?

假设:

Cache-B

挂掉。

原来:

Cache-A

      ↓

Cache-B

      ↓

Cache-C

Cache-B 负责:

100 ~ 400

删除后:

Cache-A


Cache-C

那么:

100~400

顺时针找到 Cache-C

所以:

Cache-B 数据自动迁移到 Cache-C。


8. 但是还有一个问题:数据倾斜

普通一致性 Hash:

Cache-A

        Cache-B


Cache-C

可能:

Cache-A负责 70%

Cache-B负责20%

Cache-C负责10%

原因:

节点随机落在环上。


9. 虚拟节点(Virtual Node)

解决方式:

让一个真实节点拥有多个 Hash 位置。

例如:

以前:

Cache-A

hash(Cache-A)=100

变成:

Cache-A-1

hash(Cache-A#1)=100


Cache-A-2

hash(Cache-A#2)=300


Cache-A-3

hash(Cache-A#3)=800

环:

100   A

200   B

300   A

400   C

600   B

800   A

效果:

节点分布更加均匀。

实际系统:

例如:

真实节点:

Redis-1

虚拟节点:

Redis-1#001
Redis-1#002
...
Redis-1#200

10. 一致性 Hash 数据结构

通常实现:

HashRing


          TreeMap


key(hash值)
       |
       |
       ↓

100 -> Node-A
250 -> Node-B
400 -> Node-C
700 -> Node-D

查找:

hash(key)

      |
      ↓

TreeMap.ceilingEntry(hash)

找到第一个 >= hash 的节点

例如:

key hash = 350


TreeMap:

100 A
250 B
400 C
700 D


ceilingEntry(350)

返回:

400 C

11. Redis Cluster 和一致性 Hash 的区别

很多人会混淆。

Memcached

经典:

一致性Hash

key

↓

hash ring

↓

server

Redis Cluster

不是一致性 Hash。

Redis 使用:

16384 个 slot

结构:

key

 ↓

CRC16(key)

 ↓

slot

 ↓

Redis节点

例如:

slot 1000

        ↓

Redis-A


slot 8000

        ↓

Redis-B

优势:

  • 节点迁移更可控

  • 运维方便


12. 一致性 Hash 解决什么问题?

总结:

问题 普通Hash 一致性Hash
节点增加 大量迁移 少量迁移
节点删除 大量失效 局部影响
负载均衡 较好 需要虚拟节点
实现复杂度 简单 复杂
适合动态集群

13. 一句话理解

一致性 Hash 就是在所有服务器和数据之间建立一个「哈希环」,数据顺时针找到最近的服务器;当服务器增加或减少时,只影响环上一小部分数据,从而避免大量缓存失效。


一个非常直观的类比

普通 Hash:

像:

100个人按照身份证号码 % 3 分到3个房间。

增加一个房间:

身份证 % 4

所有人重新分房。

一致性 Hash:

像:

让房间固定坐在一个圆桌旁,人根据自己的编号顺时针找最近房间。

增加一个房间:

只需要接管附近的一部分人。

这就是为什么分布式缓存(尤其 Memcached)大量使用一致性 Hash。