Redis Cluster 深潜 · 16384 个哈希槽与分片路由

作者:杨帅      发布日期:2026.08.04

Redis Cluster · 深潜

16384 个哈希槽

一个 key 如何在几微秒内,找到属于它的那台机器

Redis Cluster 不用一致性哈希环,而是把整个键空间切成 16384 个哈希槽。每个 key 用 CRC16(key) mod 16384 算出落在哪个槽,槽归某个分片所有——于是路由变成一次纯计算,没有中心节点、没有代理。

▲ key → CRC16 → 槽 → 分片routing = pure computation
foo CRC16(key) mod 16384 = 12182 0 5461 10923 16383 分片 A 分片 B 分片 C slot 12182 Shard A 0–5460 Shard B 5461–10922 Shard C 10923–16383 ✓ 命中
分片 A:槽 0–5460 分片 B:槽 5461–10922 分片 C:槽 10923–16383
01 · 为什么要分片

把键空间分散到多台机器

当数据量或写入速率超过单台 Redis 的极限,就得把键空间横向切分到多台机器上。问题是:给定一个 key,怎么快速、确定地知道它在哪台机器?

常见方案是一致性哈希环,但 Redis 选了更简单、更可控的一招:预先把键空间划成 16384 个固定的哈希槽(slot)。每个 key 属于某一个槽,每个槽归某一个主分片所有。加机器、减机器,本质上都只是在分片之间搬动槽而已。

打个比方

不是给每个包裹现算地址,而是先把城市划成 16384 个邮政编码区。每个包裹按规则算出邮编,邮编归哪个分拣中心管是固定的。要新开一个分拣中心,只需把一部分邮编划过去——包裹本身的算法从不改变。

02 · key 如何找到家

槽的计算:CRC16 取模

槽的计算是纯确定性的——同一个 key,在任何客户端、任何时刻,算出的槽都一样:

slot = CRC16(key) mod 16384  # 等价于 CRC16(key) & 0x3FFF
# CLUSTER KEYSLOT foo → 12182  CLUSTER KEYSLOT bar → 5061

因为 16384 = 2¹⁴,取模可以退化成一次按位与& 0x3FFF),快到可以忽略。智能客户端在连接时缓存一份「槽 → 节点」映射,之后本地算槽、直连目标分片,不经任何中转。

哈希标签:把相关的 key 摁到同一个槽

Redis Cluster 只允许对同一个槽内的多个 key 做多键操作(MSETMGET 等),否则报 CROSSSLOT。想让一组 key 落在一起,用哈希标签:只要 key 里有 {…},就只对花括号里的部分求哈希。

✗ 不同槽 · 可能 CROSSSLOT
user:100  → slot A
order:100 → slot B
两个 key 算出不同的槽,可能落在不同分片。对它们做 MSET/MGET 会被拒绝。
✓ 同一个槽 · 可多键操作
{user:100}.profile → slot X
{user:100}.orders  → slot X
只哈希 user:100,两个 key 必然同槽、同分片,多键操作与事务可用。
!

哈希标签是把双刃剑。 用它可以共置相关数据;但如果把太多 key 塞进同一个标签,会让某个槽(进而某个分片)变成热点,反而拖垮分布均衡。

03 · 槽的分布与重分片

扩缩容就是在分片间迁移槽

每个主分片负责 16384 个槽中的一段。要扩容,就把一部分槽从旧分片到新分片;要缩容,就把槽回去。数据随槽一起搬。

▲ 新增分片 D,从 A/B/C 各匀走一批槽reshard · migrate
重分片前 · 3 分片 A · 0–5460 B · 5461–10922 C · 10923–16383 MIGRATING 重分片后 · 4 分片(新增 D) A B C D D D 迁出方标记 MIGRATING,迁入方标记 IMPORTING,槽与其中的 key 一起搬

Redis Cloud / Redis Enterprise 上,重分片、槽迁移和自动再平衡都是托管自动完成的,扩缩容不影响应用;社区版则需要用 redis-cli --cluster reshard 手动操作。

04 · 客户端如何被纠正

MOVEDASK:两种重定向

如果客户端把命令发给了「不拥有这个槽」的节点,节点不会转发,而是回一个重定向,告诉客户端该找谁。但这里有个微妙而关键的区别。

▲ MOVED:永久 · 更新映射topology changed
Client 缓存槽映射 Node A 不拥有 slot 12182 Node C 拥有 slot 12182 ① GET foo ② -MOVED 12182 C:6379 ③ 更新映射后重试 → C ✓

MOVED永久的:它说「这个槽现在归 C 了」。智能客户端收到后会更新本地映射,以后直接发 C。这通常发生在扩缩容、故障切换之后拓扑变化时。

ASK 则是临时的,只在某个槽正在迁移途中出现——迁出节点标 MIGRATING、迁入节点标 IMPORTING。当你要的 key 已经搬到目标节点,源节点回 ASK:客户端要先发一个 ASKING,再把命令发给目标节点,而且不更新映射(因为槽还没正式易主)。

MOVED · 永久
含义:槽已易主。
动作:更新槽映射,以后直连新节点。
时机:扩缩容 / 故障切换后。
ASK · 临时
含义:槽迁移中,这个 key 已在目标。
动作:ASKING + 命令给目标,更新映射。
时机:重分片进行中。
05 · 冷知识

为什么槽数是 16384

CRC16 明明能产生 65536 种取值,为什么槽只取 16384 = 2¹⁴,而不是 65536?答案藏在节点间的心跳包里。

集群里每个节点都通过集群总线(端口 = 数据端口 + 10000,如 6379→16379)用 gossip 协议互相心跳,心跳里带着一张它拥有哪些槽的位图。位图大小 = 槽数 ÷ 8:

16384 槽
2 KB
16384 bit ÷ 8 = 2048 字节 · 心跳可频繁发送
65536 槽
8 KB
65536 bit ÷ 8 = 8192 字节 · 心跳带宽浪费

再加上一个现实判断:Redis Cluster 的节点数实际很难超过约 1000。16384 个槽已经能让上千个节点分得足够均匀,同时把心跳位图压到 2KB——这是分布粒度心跳开销之间的一个务实折中。理论上最多可有 16384 个主分片(每个 1 槽),但推荐规模在千级以内。

06 · 深水区的坑

使用集群前的注意事项

×
跨槽多键操作会被拒MSET/MGET/事务/Lua 涉及的 key 必须同槽,否则 CROSSSLOT。跨槽批量要在客户端拆分并发。
×
哈希标签用过头 = 热点把海量 key 塞进同一个 {tag},会让单个槽和分片过载,破坏负载均衡。
×
用「笨」客户端不缓存槽映射、不处理 MOVED/ASK 的客户端,每次都被重定向,延迟翻倍。用集群感知客户端。
×
忘了开集群总线端口节点间 gossip 走「数据端口 +10000」。防火墙没放行这个端口,集群无法组网。
×
误以为槽会自动挪 key 的算法槽的计算永远是 CRC16 mod 16384,迁移只搬「槽的归属」和数据,不改 key→槽 的映射。
×
丢光一个分片的所有副本某分片的主与全部副本都挂,则该分片负责的槽不可用;其余分片仍照常服务。副本要跨区。
收尾 · 一句话

路由是一次计算,而非查询

16384 个固定的槽,让「key 在哪台机器」从一次查询变成一次 CRC16 mod 16384。没有代理、没有中心目录;扩缩容只是搬槽,客户端靠 MOVED/ASK 自我纠正。简单,所以可扩展。

CRC16(key) mod 16384 → 槽 → 分片 · 搬槽即扩容,MOVED 永久 / ASK 临时,心跳位图定 16384。