分布式分片算法:一致性哈希与 Hash Slot
如果你玩过早年的网游,大概率都见过这样的界面:
- 电信一区、电信二区、网通一区;
- 新服火爆,老服鬼区;
- 你在 A 服的角色、资产、好友,天然和 B 服隔离。
这其实就是最朴素的“分布式”雏形:按服务器切分用户与数据。它简单有效,但也留下了长期代价。
这篇文章想回答两个问题:
- 为什么系统会从“分服”走向更通用的分布式分片?
- 在分片算法上,为什么 Redis 选择 Hash Slot,而不是纯一致性哈希环?
1. 为什么要做分布式:从分服到统一系统
1.1 分服时代为什么可行
“分服”本质上是业务级分片:
- 每个服有自己的数据库与状态;
- 流量按服入口自然隔离;
- 故障影响面相对可控。
在业务早期,这种方式实现成本低、组织上也好管理。
1.2 分服模式的长期代价
当业务发展到中后期,问题会逐渐显现:
- 数据孤岛:跨服好友、公会、交易、排行榜都变复杂;
- 资源利用不均:有的服爆满,有的服闲置;
- 运营复杂:跨服活动、合服、迁移都很重;
- 研发负担上升:很多功能要做“双份逻辑”(服内与跨服)。
1.3 走向分布式后的核心目标
现代分布式系统通常希望做到:
- 统一的数据与服务视图;
- 弹性扩缩容;
- 更细粒度的负载调度;
- 可控的数据迁移与故障恢复。
而这些目标最终会落到一个非常底层的问题:
f(key, topology) -> node
也就是:给定 key 和当前拓扑,如何稳定地找到目标节点。
2. 起点:hash(key) % N
最朴素的分片算法:
node = hash(key) % N
优点是简单且常数开销低;问题是 单调性差(topology 一变,映射大面积变化)。
从 N 扩到 N+1 时,可近似得到:
- 不变比例:
1/(N+1) - 重映射比例:
N/(N+1)
例如 4 节点扩到 5 节点,约 80% key 会重映射。这在缓存与在线业务场景里通常不可接受。
3. 一致性哈希:优化“扩缩容扰动”
一致性哈希把节点和 key 都映射到哈希环上,key 顺时针归属到第一个节点。
它解决的核心是:
- 拓扑变化只影响环上的局部区间;
- 新增 1 个节点时,迁移比例期望约
1/(M+1)(M为原节点数)。
为了降低倾斜,工程上常加虚拟节点(vnodes),把单节点映射成多个点位,以改善负载方差。
从算法角度看,一致性哈希的优势非常明确:
- 相比
%N,重映射规模显著降低; - 具备更好的扩展单调性。
4. 一致性哈希的代价:控制面复杂度上升
一致性哈希并非“无代价最优”。在大规模系统里常见三类成本:
- 负载平衡依赖参数
- vnode 数量、权重策略会影响倾斜程度;
-
参数不当时,热点和不均衡仍会出现。
-
路由状态更复杂
- 客户端往往要维护 ring 元数据(含 vnode);
-
拓扑频繁变化时,状态收敛与一致性管理更难。
-
重平衡可控性不够离散
- 你能迁移局部区间,但很难像“搬 200 个固定分区”那样显式操作。
所以问题从“迁移是否足够少”,变成了“迁移是否足够可控”。
5. Redis Hash Slot:把连续空间离散化
Redis Cluster 使用两级映射:
key --(CRC16 % 16384)--> slot --(slot owner)--> node
这相当于把连续哈希空间先离散为固定 16384 个桶(slot),再把桶分配给节点。
算法上,它保留了一致性哈希“局部迁移”的思想,同时把控制面变成了离散操作:
- 迁移不再是“环区间”,而是“槽位集合”;
- 重平衡可以按 slot 批次执行与观测。
6. 为什么 Redis 选择 Slot 而不是纯 Ring
6.1 迁移粒度可计算、可编排
如果迁移 K 个 slot,则受影响 key 的期望比例约为:
K / 16384
这让迁移计划天然可量化:
- 可按批次推进;
- 可限速与回滚;
- 可明确评估影响面。
6.2 路由状态规模固定
slot 总数固定,路由状态上界稳定。无论 key 规模多大,控制面复杂度主要受节点数和 slot 分配影响,而不是受 ring 参数直接驱动。
6.3 多 key 共址有显式机制
Redis 的 hash tag(如 user:{42}:profile 与 user:{42}:cart)可强制 key 进入同一 slot。
这在事务、Lua、多 key 操作中非常关键:共址从“概率事件”变成“建模能力”。
6.4 故障切换语义更直接
主从切换时,本质是 slot 归属转移。控制语义清晰,恢复路径明确。
7. 为什么是 16384(2^14)
这是典型工程折中:
- 槽位足够多:支持细粒度负载分配与迁移;
- 槽位不至过多:路由表与协议开销可控;
- 计算实现友好:取模简单、实现稳定。
槽位太少,迁移颗粒粗;槽位太多,控制面开销上升。16384 是一个长期实践后的平衡点。
8. Redis Cluster 的实际运行细节
上面讲的是算法与控制面思路。真正在线上稳定运行,还依赖一套运行时机制。
8.1 客户端路由:MOVED 与 ASK
Redis Cluster 的客户端通常会缓存 slot -> node 路由表,但这张表不是永远不变。
当客户端发到“错误节点”时,服务端会返回重定向:
- MOVED:这个 slot 的归属已经稳定改变了。客户端应更新本地路由缓存。
- ASK:这个 slot 正在迁移中。客户端本次请求先去目标节点(通常先发
ASKING),但不应立刻把它当作永久路由。
可以把它理解为:
- MOVED = “最新地址已经改了”
- ASK = “这次先去临时窗口办理”
8.2 槽位迁移:导入/导出状态与双端协作
slot 从 A 迁到 B 时,不是瞬间切换,而是一个过程:
- 配置 slot 在源节点为迁出(migrating),目标节点为迁入(importing)
- 将该 slot 下的 key 分批从 A 搬到 B
- 客户端请求期间可能收到 ASK 重定向
- 全量搬完后,更新 slot 最终归属,后续走 MOVED/新路由
这个流程的价值在于:
- 迁移可分批限速,避免一次性抖动
- 迁移期间仍可对外服务
- 客户端通过 ASK/MOVED 自适应收敛
8.3 故障检测与转移:谁来接管 slot
Redis Cluster 通过节点间 Gossip 交换状态,识别疑似下线(PFAIL)和客观下线(FAIL)。
当某主节点被确认失败后,其从节点会参与提升流程。提升成功后,本质动作是:
- 新主节点接管原主负责的 slot
- 集群广播新的 slot 归属
- 客户端路由逐步收敛到新拓扑
从分片语义看,故障转移不是“某台机器挂了”这么简单,而是“slot 所有权发生了重绑定”。
8.4 多 key 约束:为什么要 Hash Tag
在 Cluster 模式下,很多多 key 命令要求 key 位于同一 slot。否则会出现跨槽错误(常见为 CROSSSLOT)。
所以业务建模时会主动使用 hash tag:
order:{1001}:metaorder:{1001}:items
同一 {1001} 保证同槽,才能稳定支持事务、Lua 或批量操作。
8.5 热点与再均衡:算法之外的运营问题
即使 slot 分布均匀,也可能出现业务热点(某些 key 访问远高于平均值)。
实践上通常需要组合手段:
- 通过监控识别热点 slot / 热点 key
- 迁移 slot 做负载再均衡
- 业务层做热点打散或读副本分流
这说明分片算法解决的是“结构性均衡”,而不是“业务流量永远均衡”。
9. 三种算法的核心对比
| 方案 | 扩容重映射 | 负载均衡 | 控制面复杂度 | 可控迁移粒度 |
|---|---|---|---|---|
hash % N |
高(约 N/(N+1)) |
中 | 低 | 粗 |
| 一致性哈希 | 低(局部) | 中-高(依赖 vnode) | 中-高 | 中 |
| Hash Slot | 低(按 slot) | 高(可调 slot 分配) | 中(固定上界) | 细 |
Redis 的选择不是否定一致性哈希,而是把“低扰动映射”进一步工程化为“离散、可编排、可观测”的分片算法。
结语
从“分服”到“分布式”,再到具体的分片算法,演进主线很清晰:
从“先把流量拆开”,走向“在可治理前提下持续扩展”。
%N、一致性哈希、Hash Slot 并不是互相取代的教条关系,而是不同阶段对“复杂度预算”的取舍。
在分布式工程里,数学性质决定上限,而控制面的可操作性决定系统能跑多久。