一致性哈希
分布式缓存
对于经常使用的数据,我们一般会使用 Redis 作为缓存机制,为了实现高可用,使用了3台Redis(没有设置集群,集群至少要6台)。
使用hash算法,存储的时候根据公式 h = hash(key)%机器节点数,h 为 Redis 对应的编号,取数据的时候也根据相同的公式取,因此一定可以从存储的机器中拿到想要的数据。但是使用这种策略可能会存在以下问题:
- 假设有一台 Redis 服务器宕机了,此时每个 key 就要按照 h = hash(key)%(机器节点数-1) 重新计算
- 假设要新增一台 Redis 服务器,此时每个 key 就要按照 h = hash(key)%(机器节点数+1) 重新计算
也就是说,如果服务节点有变更,会导致缓存失效,大量的 key 需要重新计算,在这期间如果有请求进来,就会直接打到数据库上,导致缓存雪崩。
一致性哈希算法
一致性哈希是讲整个哈希空间组织成一个虚拟的圆环,假设哈希函数 H 的值空间为 [0,2^32-1](哈希值是32位无符号整形)。
把服务器按照 IP 或者主机名作为关键字进行哈希,确定服务器在哈希环中的位置。
再使用哈希函数把数据对象映射到环上,数据从顺时针方向找,遇到的第一个服务器就是它定位到的服务器。

结论:数据1、2存储服务器B上,数据3存储在服务器C上,数据4存储在服务器A上
容错性和可扩展性
假如这时候有服务器C宕机了呢?那么只有原本在B和C之间的数据会失效,重新定位到服务器A,其他数据节点的服务器不会发生变化。

或者我们想新增一台服务器D呢?那么只有C和D之间的数据会失效,重新定位到服务器D,而其他的数据节点的存储服务器也不会发生任何变化。

可以看出,一致性哈希算法对于节点的增减只会有一部分数据需要重新定位,不会导致大量的缓存失效。
虚拟节点
现实的业务场景中,节点不会分布得那么均匀,如果节点较少,可能会出现数据倾斜的情况。
观察下图,所有的数据全都定位到服务B上,无法实现负载均衡了。

为了解决这种数据存储不平衡的问题,一致性哈希算法引入了虚拟节点机制,即对每个节点计算多个哈希值,每个计算结果位置都放置在对应节点中,这些节点称为虚拟节点。

增加了虚拟节点到实际节点的映射,这样就能解决服务节点少时数据不平均的问题了。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。
手撕源码
介绍完一致性哈希算法的概念和规则,接下来我们从源码的角度分析一致性哈希算法是怎么实现的。
哈希算法
首先确定项目中要使用的哈希算法,其中服务器和数据的映射都依赖哈希算法。
非加密算法:MurMurHash算法
1 | /** |
加密算法:md5
1 | /** |
节点映射
以有序 Map 的形式在内存中缓存每个节点的 Hash 值对应的物理节点信息,所以引入了 TreeMap 进行存储。
为了增加一致性哈希算法中的虚拟节点,在初始化节点映射的过程中,将计算出 实际节点*虚拟节点 的hash值,以 Hash 值为 key,以物理节点标识为 value,以有序 Map 的形式在内存中缓存,作为后续计算数据对象对应的物理节点时的查询数据。代码如下,virtualHash2RealNode 中缓存着所有虚拟节点 Hash 值对应的物理节点信息。
1 | /** |
数据定位节点
已知 virtualHash2RealNode 中存放着物理节点的信息,使用 tailMap() 方法寻找到比该数据大的范围内的所有物理节点,返回第一个节点。
1 | /** |
工具类
一般在项目中,会把一致性哈希算法包装成工具类使用。
1 | public class ConsistencyHashUtil { |
hutool 工具包也有封装好一致性哈希算法的工具类,只需要传入复制的节点个数和节点对象就能初始化节点映射。
1 | /** |
传入复制的节点个数和实际物理节点信息,实现一致性哈希。
1 | public static ConsistentHash<Node> makeProxyPool(List<OpenaiProxy> openaiProxies) { |
一致性哈希局限性分析
核心特点
一致性哈希的核心特性决定了其适用边界:
1、 极低的数据迁移成本:扩容/缩容时仅影响相邻节点数据(约1/N),避免缓存雪崩
2、 虚拟节点均衡机制:通过多虚拟节点分散物理节点压力,解决数据倾斜
3、 固定哈希空间:基于2^32固定环,不随节点数变化
4、 无物理拓扑感知:无法识别机房、机架等物理位置
5、 数据无序分布:相邻Key可能分布在不同节点
适用场景
分布式缓存扩容
优势体现:
- 扩容时缓存命中率保持>90%
- 虚拟节点自动均衡负载
- 避免数据库穿透风险
典型系统:
- Memcached客户端分片
- Redis集群前期的客户端分片方案
无状态服务负载均衡
场景案例:
- Dubbo的粘滞会话调用
- API网关的用户会话路由
- 分布式Session管理
核心价值:相同参数请求始终路由到同一实例
不适用场景
多机房拓扑感知场景
问题本质:
无法感知物理拓扑,违背跨机房容灾原则
工业级解决方案:
- Ceph的CRUSH算法:层级拓扑感知
- TiDB的PD调度:基于机架位置分配Region副本
数据库范围查询
1 | SELECT * FROM orders |
一致性哈希缺陷:
- 时间相邻的数据可能分布在不同节点
- 排序操作引发跨节点数据收集
- 性能急剧下降
替代方案:
- TiDB的Range Sharding:按主键范围分片
- HBase的Region划分:基于RowKey有序分布
超大规模集群(>1000节点)
1 | # TreeMap查找时间复杂度 |
运维灾难:
- 虚拟节点管理成本指数增长
- 数据迁移效率低下
架构升级方案:

Redis Cluster 为什么选择哈希槽而非一致性哈希
先说结论, Redis Cluster的哈希槽架构方案决策基于:
1、 数据均匀分布优先:哈希槽更加有利于数据在节点上的均衡分布
2、 控制数据迁移:固定槽位作为迁移单元更易管理
3、 提升运维灵活性:支持按节点性能手动分配槽位
4、 网络带宽优化:固定槽位数降低Gossip协议开销
这种设计使Redis Cluster在保证数据均匀分布的同时,兼顾了集群管理的灵活性和性能。
Redis Cluster 的分片设计
Redis Cluster采用固定16384个槽位(slots)进行数据分片,每个槽位对应一个数据子集。
键值对通过CRC16哈希算法映射到具体槽位:
1 | slot = CRC16(key) % 16384 |
槽位与节点的映射采用静态分配机制,比如:
- 节点A:槽位0-5460
- 节点B:槽位5461-10922
- 节点C:槽位10923-16383
Redis Cluster 动态扩容机制
当新增节点时,Redis Cluster 会重新分配槽位:

通过CLUSTER ADDSLOTS命令手动调整槽位分布,实现数据均匀分布。
Redis Cluster 去中心化元数据管理
Redis Cluster采用Gossip协议实现去中心化元数据同步:

节点间通过PING/PONG报文交换槽位映射信息,最终达成集群状态一致。
槽位多,传输元数据就很大。
redis采用bitmap机制管理元数据,也就是说 哈希槽 (2^14) / 8 = 16384 大概2K。
而 如果 redis采用 一致性哈希,也就是说 槽位数 2^32 / 8 会大很多,影响心跳传输效率。
Redis Cluster 为什么不是一致性哈希?
redis选择哈希槽,而不是一致性哈希,主要基于三方面考虑
- 固定槽位数降低Gossip协议开销,槽位少心跳时传送的元数据就少,而且从实际场景中不需要那么多槽位,一般集群规模200节点以下,16348 / 200 =81足够
- 提升运维灵活性,固定槽位支持手动分配槽位
- 哈希槽更加利于数据均衡分布,而且可以手动分配节点槽位,性能好的节点可以多分配槽位

数据分布优先策略
Redis Cluster选择哈希槽而非一致性哈希的核心原因在于其数据分布优先策略:
通过固定16384个槽位与手动分配机制,优先保证数据均匀分布和精确控制,牺牲了部分扩容灵活性以换取更稳定的负载均衡,尤其适合中小规模集群(≤1000节点)。
| 维度 | 一致性哈希 | Redis槽位 |
|---|---|---|
| 核心目标 | 最小化数据迁移 | 保证数据均匀分布 |
| 扩容影响 | 仅影响相邻节点 | 全局重新分配 |
| 数据倾斜处理 | 依赖虚拟节点 | 手动调整槽位分布 |
| 适用规模 | 超大规模集群 | 中小规模集群(≤1000节点) |
16384(2^14)槽位的精妙设计
edis作者Salvatore Sanfilippo的解释:
- 网络优化:心跳包携带全量槽位信息仅需2KB(16384/8)
- 规模适配:千节点规模下每个节点仍持有足够槽位(16384/1000≈16)
- 实践经验:生产环境集群通常不超过200个节点
TiDB 为什么不使用一致性哈希
TiDB 的定位是 “让分布式数据库像 MySQL 一样好用”,它兼容 MySQL 协议与 SQL 语法,支持 ACID 事务,同时解决传统 MySQL“分片难、扩容复杂” 的问题,属于 NewSQL(新型关系型数据库)范畴,适合需要关系型数据库特性但需水平扩展的场景。
TiDB 核心架构
TiDB 采用 “三层架构”,职责清晰且解耦:
- TiDB Server:计算层(无状态),负责接收 SQL 请求、解析执行计划、优化查询,可横向扩展(增加节点即可提升并发能力),不存储数据。
- PD(Placement Driver):集群调度中心,负责管理 TiKV 的分片(Region)、调度数据迁移(如扩容时的分片均衡)、维护集群元数据,类似 “大脑”。
- TiKV:存储层(分布式 KV 存储),按 Range 分片存储数据(将 SQL 表映射为 KV 键值对),基于 Raft 协议实现副本(通常 3 副本),确保数据可靠。
TiDB 典型场景
- 传统 MySQL 分片替代:当 MySQL 单库数据量超过 100GB、并发超过 1 万 QPS 时,分片运维复杂,TiDB 可直接替代并支持无缝扩容(如电商订单库、用户库)。
- 金融核心业务:如银行转账、支付系统,需强一致性和 ACID 事务,TiDB 的 Raft 协议和分布式事务可满足需求。
- HTAP 混合场景:如零售行业的 “实时订单处理 + 实时销售报表”,无需分别部署 MySQL 和 Hive,TiDB 可同时承载事务和分析负载。
TiDB 放弃一致性哈希的根本原因
- 一致性哈希适合“只点查、不范围查、不讲究容灾”的场景(如缓存、对象存储);
- 而 TiDB 作为分布式关系型数据库,必须支持范围查询 + 强一致性 + 容灾隔离;
- 所以一致性哈希的“拓扑盲区”和“范围查询失效”是架构级致命缺陷
拓扑盲区:无法感知物理拓扑,击穿数据容灾底线
一致性hash只关心:
- 节点哈希到环上,数据按哈希值顺时针找最近的节点。
- 增加/删除节点只影响局部数据,迁移量小。
一致性hash的拓扑盲区: 完全不知道“节点在哪” —— 不关心机房、机架、交换机、电力域。拓扑盲区:副本“瞎放”,容灾“白搭”
一致性哈希的核心问题是 “只认哈希值,不认物理位置”—— 它将节点抽象成环上的 “哈希点”,分配数据时仅根据 Key 的哈希值找环上最近的节点,完全忽略节点实际部署的机房、机架、机柜等物理拓扑信息。
这一 “拓扑盲区” 直接导致副本部署失控,最终引发数据不可用风险。
例如:
- 假设将 3 个 TiDB 存储节点(Node1、Node2、Node3)哈希到环上,Node1 和 Node2 恰好被哈希到相邻位置,且二者实际部署在同一机房 A,Node3 部署在机房 B;
- 当分配某条数据的 3 个副本时,一致性哈希会按 “环上最近” 原则,将副本 1 存 Node1、副本 2 存 Node2、副本 3 存 Node3—— 此时 2 个副本集中在机房 A,仅 1 个在机房 B。
TiDB 作为数据库,数据可用性是底线,必须通过 “跨机房 / 机架部署副本” 实现容灾(比如 1 个副本在机房 A、1 个在机房 B、1 个在机房 C),确保单一机房断电 / 断网时,仍有副本可用。
但一致性哈希的拓扑盲区会导致:
- 物理集中风险:多个副本被分配到同一机房 / 机架,形成 “单点故障链”—— 若机房 A 断电,Node1 和 Node2 同时不可用,仅剩的 Node3 副本若再故障,数据直接丢失;
- 无法主动控制副本拓扑:TiDB 无法强制要求 “副本必须跨机房部署”,因为一致性哈希的分配逻辑完全由哈希值决定,无法干预物理位置关联。
对 TiDB 而言,“数据不可用” 是致命故障,而拓扑盲区直接让容灾设计形同虚设,这是其放弃一致性哈希的首要原因。
范围查询失效:破坏 Key 自然顺序,拖垮关系型查询性能
一致性哈希的 范围查询 做法: 对 Key 做哈希(如 hash(user_id)),打散到整个环上。 结果:逻辑相邻的 Key,物理上可能天各一方。
一致性哈希为了“负载均衡”牺牲了局部性原则,导致范围查询变成分布式全表扫描,这在 OLTP/HTAP 系统中是不可接受的。
TiDB 作为关系型数据库,“范围查询” 是高频核心场景(如SELECT * FROM order WHERE id BETWEEN 1000 AND 2000、SELECT * FROM log WHERE create_time >= '2025-01-01')。
但一致性哈希的 “哈希分配逻辑” 会彻底破坏 Key 的自然顺序,导致相邻 Key 分散存储,最终让范围查询陷入 “多节点扫描 + 跨网合并” 的性能泥潭。
例如:
- Key 的自然顺序是
id=100 → id=101 → id=102 → id=103; - 经哈希计算后,可能的分配结果是:
id=100存 Node1、id=101存 Node3、id=102存 Node2、id=103存 Node1; - 最终,“100~103” 这个连续的 Key 范围,被拆分成 Node1(100、103)、Node2(102)、Node3(101)三个节点存储,完全失去 “连续性”。
直接后果:范围查询需跨节点合并,性能雪崩
TiDB 的解法:Range-based 分片(Region)
iDB 的存储层 TiKV 采用按范围分片(Region):
- 数据按主键范围划分为连续区间(如
[start, end))。 - 每个 Region 默认 96MB,三副本,通过 PD(Placement Driver) 调度。
- PD 感知拓扑(机房、机架、主机),强制副本隔离(如跨机房、跨机架)。
- 范围查询只需访问少量 Region,性能线性扩展。
TiDB的Range分片机制
TiDB 集群由 PD + TiKV + TiDB Server 构成。

关键设计:
1、 动态分片:Region默认96MB,自动拆合
2、 负载感知:QPS/容量超阈值触发调度
3、 Raft副本:多副本保证高可用
TiDB分片流程:

TiDB的拓扑感知
TiDB通过PD(Placement Driver)实现智能副本分布:
副本放置规则:
(1) 跨机房部署(如A机房2副本,B机房1副本)
(2) 同机房跨机架
(3) 规避单点故障

Ceph为什么不用一致性哈希
Ceph 的核心价值是打破存储类型壁垒,用一套系统同时提供对象存储(兼容 S3/Swift)、块存储(类似磁盘,供虚拟机 / 容器使用)、文件存储(兼容 POSIX,供应用直接挂载),避免企业部署多套存储系统的成本浪费。
Ceph 核心架构
Ceph 集群由三大核心组件构成,各司其职:
- Monitor(监控节点):维护集群拓扑(CRUSH Map)、配置信息和集群状态,确保数据分配规则正确执行(无数据存储功能)。
- OSD(对象存储节点):实际存储数据的单元(通常对应物理磁盘),负责数据的读写、复制和故障恢复,是集群的 “存储肌肉”。
- MDS(元数据服务器,可选):仅用于文件存储场景,管理文件系统的元数据(如目录结构、文件权限),对象 / 块存储无需 MDS。
Ceph 典型场景
- 云平台后端存储:为公有云 / 私有云提供虚拟机磁盘(块存储)、对象存储服务(如图片 / 视频存储),例如 OpenStack 默认集成 Ceph。
- 海量数据备份归档:企业备份数据(如数据库备份、日志文件)、归档数据(如医疗影像、监控录像),支持按对象生命周期管理(自动冷热数据迁移)。
- 混合云存储对接:因兼容 S3 API,可作为私有云存储与 AWS S3、阿里云 OSS 等公有云存储对接,实现数据同步或容灾。
一致性哈希在分布式存储的致命伤
要理解 Ceph 放弃一致性哈希的核心原因,需先明确二者的定位差异:一致性哈希为小规模、简单分布式场景(如 Memcached 缓存) 设计,核心目标是 “减少节点上下线时的数据迁移量”.
而 Ceph 是企业级大规模分布式存储系统,核心诉求是 “数据高可靠(跨拓扑容灾)、集群自动化运维(免人工干预)”。
| 维度 | 一致性哈希 | CRUSH |
|---|---|---|
| 输入 | object-key | object-key + 拓扑树 + 规则 + 权重 |
| 输出 | 一个节点 | 一组满足“隔离约束”的 OSD |
| 是否感知机房/机架 | ❌ | ✅ 逐层强制隔离 |
| 是否可人工指定“别放同机架” | ❌ | ✅ 规则引擎 |
| 加节点是否自动再平衡 | ❌ 需手动触发 | ✅ 自动、仅迁移必要数据 |
| 副本故障是否重新计算 | ❌ 需外部脚本 | ✅ 客户端本地 5ms 内算出新 OSD |



