一致性Hash算法是一个为了解决分布式系统中经典难题而生的、非常精巧且实用的算法。
一、核心问题:为什么要有一致性Hash?
我们先看一个没有一致性Hash时的问题,这能让你明白它存在的价值。
假设我们有一个巨大的缓存系统,数据太多,一台Redis服务器存不下。我们决定用3台Redis服务器(节点)来分担存储压力。那么,如何决定一条数据(例如,键为 user:1001)该存到哪台服务器上呢?
最直观的方法:Hash取模
- 公式:
服务器下标 = hash(Key) % 服务器数量 - 例子:
hash("user:1001") % 3 = 2,那么这条数据就存放在第3台服务器上。
这看起来很好,但存在一个致命缺陷:扩容或缩容时,缓存会“雪崩” 。
假设现在3台服务器已经存了1亿条数据。这时需要增加一台服务器变成4台。
- 取模公式变成了:
hash(Key) % 4 - 这意味着绝大多数数据计算出的新下标都和原来不一样了。例如,原来
hash("user:1001") % 3 = 2,现在hash("user:1001") % 4很可能不等于2。 - 后果:当应用来请求
user:1001时,请求会被路由到新的服务器(比如第4台),但那里根本没有这条数据!缓存全部失效,所有请求会瞬间穿透到后端数据库,数据库毫无防备,很可能直接被巨大的流量冲垮。
传统Hash取模就像固定数量的快递柜, 你有3个快递柜(节点),根据包裹单号(Key)除以3的余数决定放哪个柜子。现在增加第4个柜子,规则变成了除以4的余数。这意味着几乎所有包裹都要被重新取出,然后按照新规则放入新的柜子里。这是一个工作量巨大且服务会中断的过程。
一致性Hash算法的目标就是在增加或减少服务器时,尽可能地减少需要重新映射的数据量,避免缓存雪崩。
二、一致性Hash算法原理--环状结构
一致性Hash算法提出了一个非常聪明的构想:不再对服务器的数量取模,而是构建一个巨大的虚拟环。
-
构建哈希环。想象一个由
0到2^32 - 1组成的圆环。首尾相连。 -
将节点(服务器)放到环上。对每个服务器节点(如它的IP或主机名)计算哈希值,这个哈希值会落在环的某个位置上。假设我们有3台服务器:Node-A, Node-B, Node-C。通过计算,它们被放置到环上。
-
将数据(Key)放到环上。对每个要存储的数据的Key(如
user:1001)计算哈希值,也将其映射到环上。 -
为数据寻找节点。从数据的Key在环上的位置出发,顺时针方向寻找,遇到的第一台服务器,就是它应该存放的服务器。如下图,Key-1 找到 Node-A,Key-2 找到 Node-B,Key-3 找到 Node-C。
三、一致性Hash算法如何解决扩容问题?
现在,我们来看看最精彩的部分:新增一台服务器 Node-D 会发生什么。
- Node-D 根据其IP计算哈希值,并被插入到哈希环的某个位置(假设在Node-B和Node-C之间)。
- 受影响的只有原本顺时针属于Node-C,但现在更靠近Node-D的这一部分数据。
- 我们只需要将这一部分数据从Node-C迁移到新的Node-D上即可。
- 其他所有的数据,例如原本在Node-A和Node-B上的,完全不受影响。
对比之前的Hash取模法(几乎全部数据要重新迁移),一致性Hash算法只影响了环上的一小部分数据,完美地避免了缓存雪崩。
一致性Hash就像圆桌吃饭,服务器是坐在圆桌旁的人,数据是端上来的菜。上菜规则是:每道菜顺时针往下传,遇到谁就放在谁面前。
- 原来3个人(A,B,C)吃饭。
- 现在新来一个人D,坐在了B和C之间。
- 受影响的只有:原本从B这边传过来、本该传给C的菜,现在会先传到D面前。
- 不受影响的是:A和B之间的菜,依然传给B;C和A之间的菜,依然要绕过桌子传给A。
- 只需要把D和C之间那几盘菜重新分配一下即可,绝大部分菜都不用动。
四、虚拟节点:解决数据倾斜问题
基础的一致性Hash算法还有一个潜在问题:数据倾斜。如果服务器节点在环上分布不均匀,就会导致大量数据集中在某个节点上,而其他节点很空闲。这就失去了负载均衡的意义。
解决方案就是:虚拟节点。不再将一台服务器映射到环上的一个点,而是映射到多个虚拟节点上。例如,对Node-A,不是计算 hash("Node-A"),而是计算 hash("Node-A#1"), hash("Node-A#2"), hash("Node-A#3") ... hash("Node-A#1000"),将这1000个虚拟节点放到环上。对Node-B、Node-C也做同样的事。由于哈希函数的特性,这些虚拟节点会均匀地分散在环上。这样,即使真实服务器很少,它们在环上也拥有了大量的、分布均匀的代表点。数据通过虚拟节点最终会近乎均匀地映射到真实的物理服务器上。
五、实际应用案例
一致性Hash算法是构建大型分布式系统的基石技术之一,以下是它常见的几个实际应用场景。
- 分布式缓存:Memcached和Redis集群等片模式的核心算法。扩容缩容时影响最小。
- 负载均衡:Nginx、Dubbo等负载均衡器可以用它实现会话保持(Sticky Session),将同一客户端的请求总是转发到同一台后端服务器。
- 分布式数据库/存储系统:如Cassandra、DynamoDB,用它来做数据分片(Sharding),决定数据存储在哪个分片上。
- CDN网络:用于将用户请求路由到最近的边缘节点。
六、总结
| 特性 | 传统Hash取模 | 一致性Hash |
|---|---|---|
| 伸缩性 | 极差:节点数变动时,几乎所有数据需要重映射,导致缓存雪崩。 | 极好:节点变动仅影响环上相邻部分数据,缓存命中率高。 |
| 数据均衡 | 均衡 | 基础版可能倾斜,需引入虚拟节点技术来保证均衡。 |
| 复杂度 | 简单 | 相对复杂 |