主从复制加哨兵机制解决了读写分离和故障转移,但解决不了单机容量瓶颈。本文从"为什么需要 Cluster"切入,按照"数据怎么分片 → 客户端怎么路由 → 怎么不停机扩容 → 节点怎么通信 → 怎么故障转移 → 多 key 怎么处理"的思路,系统讲清楚 Redis Cluster 的核心机制,最后回答为什么槽的数量是 16384。


一、主从+哨兵之后,还差什么

在上一篇文章中,我们讲清楚了 Redis 主从复制和哨兵机制:

  • 主从复制解决了读性能瓶颈和数据冗余问题。
  • Sentinel 哨兵机制解决了主节点故障后的自动切换问题。

看起来高可用的核心问题已经被解决了,但实际上还有一个被忽略的问题:容量

1.1 单机 Redis 的容量上限

即使部署了一个主节点加五个从节点,整个 Redis 能存储的数据量仍然只等于主节点上的数据量。从节点保存的只是副本,并不会让总数据量变大。

换句话说:

主从架构扩展的是读写能力,而不是存储容量。

而单机 Redis 的存储量是有上限的。Redis 的数据主要存在内存中,当数据量大到一定程度(比如超过 64GB)时,fork 和 RDB 的生成会变得非常慢,很可能卡死主线程,拖慢整个节点的响应速度。

这就引出了一个新的核心问题:

如何突破单机内存限制,让 Redis 能存储更多数据?

1.2 切片思想:把数据分散到多个节点

答案是数据分片(Sharding)——把数据切成几片,分散存储到多个节点上,每个节点只负责一部分数据。

Redis Cluster 就是这种切片思想的一种实现。它不是一个简单的主从集群,而是一个去中心化的分布式集群:多个主节点各自负责一部分数据,同时每个主节点可以有自己的从节点做备份。

1.3 Cluster 引入后的六大核心问题

引入数据分片后,新的问题随之而来:

  1. 数据怎么分片:数据按什么规则分配到不同节点?
  2. 怎么实现不停机扩容:集群节点变动时,如何让客户端无感知?
  3. 客户端怎么知道命令发给谁:客户端如何确定某条命令该发往哪个节点?
  4. 节点之间怎么通信:集群中的节点如何感知彼此的状态变化?
  5. 怎么实现故障转移:某个主节点挂了,如何自动切换?
  6. 多 key 命令怎么处理:一次操作多个 key,但这些 key 分布在不同节点怎么办?

本文接下来就围绕这六个问题展开。


二、数据怎么分片:哈希槽机制

2.1 朴素方案:直接对节点数取模

Redis 中的数据都是 KV 结构,所以数据分片本质上就是对 key 分片。

最简单的方法是:用哈希函数算出 key 的哈希值,再对节点数取模。

1
slot = hash(key) % node_count

比如有 3 个节点,hash(key) = 10,则 10 % 3 = 1,数据存到节点 1。

这个方法简单直接,但有一个致命问题:集群节点数一旦变化,映射结果全部失效

比如集群从 3 个节点扩容到 5 个节点:

  • 之前 10 % 3 = 1,数据在节点 1。
  • 现在 10 % 5 = 0,数据应该在节点 0。

但数据实际上还在节点 1 上,客户端按新规则去找节点 0,就找不到了。

节点变动导致大量数据需要重新迁移,而且迁移期间客户端根本不知道数据在哪。

所以直接对节点数取模的方案不可行,需要引入一个中间层来解耦。

2.2 引入中间层:哈希槽

回顾前面的问题:直接对节点数取模之所以失败,是因为"Key 到节点"这条映射关系把两件事绑死在了一起——Key 的归属和节点的数量强耦合,节点数一变,所有 Key 的归属全变。

所以解决思路是解耦:把一条直接映射拆成两段,中间加一个中间层。

这个中间层就是槽(Slot)。Redis Cluster 定义了 16384 个槽,编号从 0 到 16383。

解耦之后,原来的一条映射变成了两段独立的映射:

  • 第一段:Key → 槽。使用 CRC16(key) % 16384 计算出 key 属于哪个槽。这一段只依赖 key 本身和固定的槽总数,和集群有几个节点完全无关,所以这个映射关系是永久固定的。
  • 第二段:槽 → 节点。每个槽分配给某个主节点负责。这一段会随集群规模变化而调整,但变化频率很低。

解耦的核心价值在于:两段映射可以独立变化。扩容或缩容时,只需要调整第二段"槽 → 节点"的映射关系,把某些槽从旧节点改派给新节点,而第一段"Key → 槽"的计算方式完全不需要改动。

换句话说,Key 永远属于同一个槽,变的是这个槽归哪个节点管。数据迁移的粒度就变成了"搬整个槽",而不是"重新计算所有 key 的归属"。

2.3 槽的初始分配

集群初始化时,16384 个槽会被平均分配到各个主节点上。比如有 3 个主节点 A、B、C:

节点 负责槽范围 槽数量
节点 A 0 ~ 5460 5461
节点 B 5461 ~ 10922 5462
节点 C 10923 ~ 16383 5461

每个节点都会在本地维护一份完整的"哪个槽归谁管"的映射表,并且节点之间会不断同步这个信息,保证所有节点看到的映射关系是一致的。


三、客户端怎么知道给哪个节点发命令

3.1 从上一节的成果说起

上一节我们通过哈希槽解耦了 Key 和节点:Key 永远属于固定的槽,槽再映射到具体节点。这样一来,扩容时只需要调整"槽 → 节点"的映射,不会再出现"数据找不到"的问题。

但这只解决了数据该放在哪的问题,还有一个问题没解决:客户端怎么知道该把命令发给谁

在哨兵模式中,这个问题是现成答案:客户端通过 Sentinel 集群来获取主节点地址,Sentinel 负责管理命令的发送目标。

但在 Redis Cluster 中并没有引入 Sentinel 机制(原因后面会详细讲),所以不能再依赖哨兵来路由命令。客户端必须自己想办法确定命令该发给哪个节点。

3.2 客户端本地缓存映射表

Redis Cluster 采取了一种更轻量的方案:

  1. 客户端启动时,连接集群中的任意一个节点,拉取一份完整的"槽 → 节点"映射表。
  2. 客户端在本地对 key 执行同样的 CRC16(key) % 16384 运算,算出 key 属于哪个槽。
  3. 根据本地映射表,找到负责该槽的节点。
  4. 直接把命令发给该节点。

这个方案高效且简单,但带来了一个新的问题:映射表会变

当集群扩容或缩容时,槽的归属会发生变化,客户端本地的映射表就可能过期。这就引出了下一节要讲的 MOVED 和 ASK 重定向机制。


四、怎么实现不停机扩容:MOVED 与 ASK 重定向

4.1 问题:迁移期间的命令路由

当新节点加入集群时,需要把一部分槽从旧节点迁移到新节点。这个迁移过程不是瞬间完成的,需要逐个 key 转移。

在迁移期间,会出现一个"窗口期"——部分数据已经搬走了,部分数据还在旧节点上。此时如果客户端的命令请求过来,它还是根据本地映射表把命令发给旧的节点。

旧节点收到命令后,有两种情况:

  • 数据还在旧节点:直接处理返回。
  • 数据已经迁移走:需要新的机制来处理。

4.2 槽位完整迁移完成后:MOVED 重定向

如果一个槽的数据已经全部迁移完成,即源节点上该槽的所有 key 都已经搬到目标节点,并且槽的归属已经正式切换,此时旧节点会返回一个 MOVED 命令:

1
(error) MOVED 100 192.168.1.2:6379

含义是:槽 100 的数据已经迁移到 192.168.1.2:6379,请去新节点找。

客户端收到 MOVED 后会做两件事:

  1. 更新本地映射表,把槽 100 的归属改为新节点。
  2. 重新发送命令到新节点。

这里需要补充一个客户端实现的细节。在 Redis Cluster 协议层面,MOVED 只是告诉客户端"槽 100 归新节点了",客户端理论上只需要更新本地映射表中槽 100 这一条记录就够了。

但在实际的主流客户端(如 Jedis、Lettuce)中,收到 MOVED 后通常不会只更新单个槽,而是会主动向服务端发起一次 CLUSTER SLOTSCLUSTER NODES 命令,直接拉取一份完整的最新映射表来整体替换本地缓存。原因是:既然集群已经发生了槽位变动,大概率不止一个槽变了,与其逐个等 MOVED 被动更新,不如一次性主动拉全量,避免后续不断触发重定向。

所以完整的流程是:协议层面 MOVED 只更新对应槽 → 客户端层面做优化,主动拉取完整映射表整体替换。

这样下次再访问槽 100 的数据时,客户端就直接找新节点,不会再去旧节点碰壁。

4.3 迁移进行中:ASK 重定向

如果数据还在迁移过程中(尚未完成),旧节点会返回一个 ASK 命令:

1
(error) ASK 100 192.168.1.2:6379

含义是:槽 100 正在迁移,你要找的数据可能在新节点上。

客户端收到 ASK 后的处理与 MOVED 完全不同:

  1. 不更新本地映射表。因为迁移还在进行中,各节点间的映射表也是动态更新的,此时更新本地映射表不准确。
  2. 向新节点发送 ASKING 命令。这一步非常关键——如果不发送 ASKING,新节点会认为这个槽还不归自己管,直接返回 MOVED,导致客户端在两个节点之间反复跳转。
  3. 在新节点上执行原命令

下面用一张完整的流程图对比三种情况,从上到下分别对应:数据未迁移、迁移已完成、迁移进行中。关键区别在于:MOVED 会更新本地映射表,ASK 不会更新但需要先发 ASKING。

4.4 MOVED 与 ASK 的核心区别

对比项 MOVED ASK
触发时机 数据迁移已完成 数据正在迁移中
映射表状态 稳定,各节点已同步 不稳定,正在动态更新
是否更新本地映射表
是否需要 ASKING

这套机制使得 Redis Cluster 可以在不停止服务的情况下完成扩容和缩容,客户端在迁移期间也能正常访问数据。


五、数据迁移过程详解

理解了 MOVED 和 ASK 之后,我们来看数据到底是怎么迁移的。

5.1 迁移的整体流程

  1. 协调计算:协调者会进行精确计算,让所有节点的槽尽量平均分配。比如扩容了一个新节点 D,协调者决定将 Slot 5000~5460 从 Node A 迁移到 Node D。
  2. 逐槽迁移:一个槽一个槽地转移内容,而不是一次性全部搬走。
  3. 标记状态:迁移过程中,源节点和目标节点都会标记该槽为"迁移中"状态,以便正确处理 ASK 重定向。

5.2 Redis 7.0 之前:扫描全量 Key

在 Redis 7.0 之前,迁移一个槽需要:

  1. 扫描源节点上的所有 key。
  2. 对每个 key 执行 CRC16(key) % 16384
  3. 只保留结果等于目标槽号的 key,丢弃其余所有 key。
  4. 把这些 key 转移到目标节点。

这个方法的问题很明显:每次迁移一个槽,都要扫描全部 key 并重新计算哈希,效率很低。当节点上的 key 数量很多时,扫描和计算的开销非常大。

5.3 Redis 7.0 之后:槽位索引结构

Redis 7.0 引入了专门的槽位索引结构进行了优化:

  • 每个槽位在内存中维护了一个独立的 Key 集合(Listpack / Intset)。
  • 通过 slot_to_keys[slot_id] 这个指针,可以直接访问属于该槽的所有 Key。
  • 迁移时无需哈希计算,无需过滤,直接遍历该集合即可拿到属于目标槽的所有 Key。

这个优化把迁移复杂度从"扫描全部 key + 哈希计算"降低到"直接遍历目标槽的 key 集合",大幅提升了迁移效率。


六、节点之间怎么通信:Gossip 协议

解决了数据分片、客户端路由和不停机扩容之后,下一个要解决的问题是:集群中的节点之间怎么通信

节点必须知道彼此的信息——新节点加入、主从切换、节点宕机、槽在迁移——才能在故障发生时做出正确的判断。

6.1 两种通信思路:中心化 vs 去中心化

对比项 中心化 去中心化
核心思想 有一个单独的管理员角色管理整个集群 节点之间互相告知,逐步同步信息
信息传播速度 较慢
运维复杂度
典型方案 Sentinel 哨兵机制 Gossip 协议

Sentinel 用的就是中心化方案,Redis Cluster 用的则是去中心化的 Gossip 协议。

6.2 为什么 Redis Cluster 不用哨兵机制

理解了中心化和去中心化的区别之后,自然会问:为什么 Redis Cluster 不继续用哨兵机制?

哨兵机制在主从架构下用得很好,但放到 Cluster 场景下会暴露出很多问题。核心矛盾在于:Cluster 的规模和复杂度远超主从架构,中心化管理模式会扛不住。具体可以从以下几个层面理解:

第一,功能集中导致运维复杂度爆炸。

在主从架构下,Sentinel 只需要管"一个主节点挂了选谁顶上"这一件事。但到了 Cluster,Sentinel 要管的事情就多了:客户端该连哪个节点、某个槽归谁管、槽迁移怎么协调、主从切换怎么配合槽的重新分配、新节点加入怎么分槽……所有关键操作都要经过这个中心。功能一多,Sentinel 的逻辑就变得极其复杂,非常容易出错。

第二,自身高可用变成新的负担。

既然所有操作都由 Sentinel 接管,那它一定不能宕机,必须多节点部署。但 Sentinel 集群自己也需要选主、需要心跳检测、需要故障转移——相当于在 Redis 集群之上又叠了一套集群。集群一大,主从切换和槽迁移频繁触发,Sentinel 集群自身的维护开销就不小。

第三,心跳检测的网络开销随节点数线性增长,容易压垮中心节点。

Sentinel 需要对每个 Redis 节点进行心跳检测。假设有 1000 个节点,Sentinel 必须收到每个节点的回复才能判断集群状态。这意味着每轮心跳检测就要处理 1000 份回复。一旦某些节点网络延迟,回复就会堆积在 Sentinel 这里。节点越多,堆积越严重,最终 Sentinel 自己就被压垮了——这和它要保护的 Redis 集群一样,成了新的瓶颈。

第四,故障切换延迟高,效果不一定好。

中心化方案下,所有决策都要经过 Sentinel 中转。当集群节点很多时,Sentinel 收集信息、做出判断、下发指令的整个链路变长,故障转移的延迟也随之增加。而去中心化方案让节点之间直接协商,反而更快。

正因为这些问题,Redis Cluster 选择了去中心化的 Gossip 协议。

6.3 Gossip 协议的工作机制

Gossip 协议的核心思想很简单:

每个节点周期性地随机挑选几个邻居节点,把自己知道的消息告诉对方。每个节点都这样做,消息就会像传染病一样扩散开来,最终所有节点都知道了集群的完整信息。

这个机制的优点是运维简单,不需要额外部署管理组件;缺点是消息传播有一定延迟,但这个延迟在集群规模可控的情况下是可以接受的。

6.4 四种消息类型

Gossip 协议规定了四种消息类型:

1. PING

节点每秒都会向其他节点发送 PING,里面附带自己知道的集群状态——哪些节点是活的、哪些槽属于哪些节点。这是信息传播的主要载体。

2. PONG

确认信息。接收到 PING 的节点会回复 PONG,告知对方"我收到了,我还活着"。

3. MEET

当有新节点加入集群时,集群中的一个老节点会向新节点发送 MEET。新节点收到后与老节点建立连接,然后开始发送 PING,实现传播式更新。当新节点通过 PING 发现了其他未连接的老节点,会主动与它们建立连接,最终让整个集群的每个节点都知道新节点的存在。

4. FAIL

当某个节点被判定为彻底挂了,会有节点向整个集群广播 FAIL 消息,告诉大家"这家伙真的不行了,把它的状态更新成下线吧"。

6.5 集群总线端口

节点之间交换 Gossip 消息用的不是 6379 端口。6379 是客户端连接的业务端口,集群有一个专门的集群总线端口(Cluster Bus)

约定规则是:业务端口 + 10000。如果业务端口是 6379,那么集群总线端口就是 16379。

这意味着部署 Redis Cluster 时,除了开放业务端口,还需要开放对应的集群总线端口,否则节点之间无法通信,集群无法搭建。

6.6 集群规模上限:为什么不超过 1000 个主节点

官方建议 Redis Cluster 的主节点不要超过 1000 个。虽然 Gossip 的去中心化方案减小了运维难度,但集群一大,每秒发送的 PING 携带的信息就多,网络开销随之增大。1000 个节点相互打招呼,网络开销已经不小。不过在 1000 个节点规模下,Gossip 仍然是可控的——每个节点只和少数几个邻居通信,不会因为集群规模增大而出现单点瓶颈。


七、怎么实现故障转移

理解了节点间通信之后,就可以看故障转移了。

在 Redis Cluster 中,从节点默认不提供读写服务,只作为主节点的数据备份。一旦主节点挂了,从节点直接顶上去。

为什么从节点默认不提供读写服务?因为主从复制是异步的,从节点的数据可能滞后于主节点。如果允许从节点处理读请求,客户端就可能读到旧数据。在 Cluster 中这个问题更敏感——数据已经被分散到多个分片,如果某个分片的从节点返回了过期数据,业务层很难察觉。所以默认情况下,所有读写都走主节点,保证读到的是最新数据。至于从节点,它的唯一职责就是随时准备在主节点故障时顶上去。

故障转移分为两个阶段:故障发现故障恢复

7.1 故障发现:从主观下线到客观下线

主观下线(PFAIL)

节点之间每秒互相发送 PING。如果某个节点在 cluster-node-timeout(默认 15 秒)内没有收到对方的回复,就会把对方标记为 PFAIL(Possibly Failing),即主观下线。

然后在下一次 PING 时,把这个 PFAIL 信息一起携带给附近的节点。

客观下线(FAIL)

这里有一个关键问题:在哨兵机制中,哨兵会主动记录投票数。但集群里面没有哨兵,只有认为某个节点客观下线时才会广播 FAIL 消息。那这个投票数是怎么记录的?

答案是:**每个节点都会维护一张"下线报告表"**,并在每次 PING 时把自己这张表的内容带给对方,对方收到后再更新自己的表。这就实现了投票信息的扩散。

用具体例子来说明整个过程:

假设集群有 5 个主节点 A、B、C、D、E,其中 Node A 真的挂了。

第一步:Node B 发现 Node A 没回应。

Node B 每秒向 Node A 发 PING,超过 cluster-node-timeout(默认 15 秒)没收到回复,就在自己的下线报告表里记录:

1
2
Node B 的下线报告表:
Node A → [Node B 认为它下线(PFAIL)]

第二步:Node B 在下一次 PING 时把这个信息带给 Node C。

Node B 向 Node C 发送 PING,消息里携带了自己的下线报告表。Node C 收到后,更新自己的表:

1
2
Node C 的下线报告表:
Node A → [Node B 认为它下线(PFAIL)]

注意:Node C 只是记录了"Node B 认为 Node A 下线"这件事,不会主动去 PING 一下 Node A 验证。原因后面讲。

第三步:Node D 也发现 Node A 没回应,同时收到了 Node C 的报告。

Node D 自己 PING Node A 也超时了,在表里加上自己的投票。同时它收到了 Node C 的 PING,把 Node B 的投票也记进来:

1
2
Node D 的下线报告表:
Node A → [Node B 认为下线, Node C 认为下线, Node D 认为下线]

第四步:投票数过半,广播 FAIL。

当某个节点的下线报告表中,认为 Node A 下线的节点数量超过了半数(这里是 3 个,超过 5/2=2.5),就判定 Node A 客观下线,向整个集群广播 FAIL 消息。

回到前面那个关键点:为什么收到 PFAIL 信息的节点不主动去验证?

因为如果每个收到"Node A 可能下线"消息的节点都主动去 PING 一次 Node A,就会在网络上引发消息风暴。假设集群有 1000 个节点,其中 500 个节点都收到了这个消息,就会瞬间多出 500 次 PING。集群越大,风暴越夸张,远超正常的网络开销。所以节点只是单纯地记录,不做额外探测,只等自己正常的 PING 轮到 Node A 时自然去判断。

7.2 故障恢复:类似 Raft 的投票选主

广播 FAIL 之后,就开始真正的故障转移。这里采取的是类似 Raft 的投票机制,大致分为四步:

第一步:资格检查

不是所有从节点都有资格参与选举。对于很久未与主节点同步的从节点会被排除。判断标准是配置项 cluster-replica-validity-factor,如果从节点的数据落后太多,就不具备参选资格。

第二步:按数据同步进度排队

通过资格检查的从节点,会根据自己的数据同步进度进行排队。同步进度越靠前的,排名越靠前。

第三步:按顺序拉票

排在第一位的从节点会向所有拥有槽的主节点发送拉票通知。这个机制很巧妙,我们需要理解它为什么能选出"数据丢失最少、网络最通畅"的节点:

为什么数据丢失最少? 因为排队依据是数据同步进度。同步进度靠前,说明这个从节点已经收到了主节点最近的大部分写入。选它当新主,主节点故障前还没来得及同步的那部分数据就最少,数据丢失自然最少。

为什么网络最通畅? 因为只有同步进度靠前的从节点才有资格先拉票。如果某个从节点网络不好、和主节点的同步长期滞后,它的同步进度就会靠后,排在后面,只有前面的从节点都失败(比如没拿到半数票)才会轮到它。这等于自动过滤掉了网络不稳定的节点。

所以这个机制同时兼顾了"数据完整性"和"网络稳定性"两个维度。

第四步:收集选票并当选

投票规则是:

  • 只有拥有槽的主节点可以投票
  • 每个主节点只能投一票。投了这个从节点后,就不能再投给其他从节点。

如果某个从节点的票数超过半数,它就当选为新的主节点。

新主节点当选后,会广播一条消息告知整个集群,然后更新各个节点的本地槽与节点映射表。这样下次客户端发送命令时,就会被 MOVED 重定向到新主节点,客户端也会更新自己的本地映射表。

7.3 故障转移的极端情况

选票未过半: 如果因为网络状况导致选票丢失,没有一个从节点的票数超过半数,就会进行合理的停滞之后再次重新投票。这和 Raft 的选举超时重试机制类似。

迁移期间命令被拒绝: 在主从切换期间,相关槽的命令会被拒绝。

数据丢失风险: Redis 主从同步是异步复制,所以极端情况下可能出现数据丢失——主节点处理完命令、回复 OK 给客户端之后,还没来得及同步给从节点就挂了。这种概率很低,但确实存在,这是异步复制机制的一定取舍。


八、多 key 操作与读写分离

8.1 多 key 命令的问题

在单节点 Redis 上,多 key 操作没有问题:

1
MSET user:1:name xiaolin user:1:age 18

但在 Redis Cluster 中,这两个 key 可能被分配到不同的槽、不同的节点。而客户端只会通过一个 pipeline 把命令传到一个节点,所以这种跨节点的多 key 命令会直接报错。

8.2 解决方案:Hash Tag

Redis Cluster 提供了 Hash Tag 机制来解决这个问题。在 key 中使用花括号 {} 括起一部分内容,只有花括号内的内容会参与 CRC16 运算和取余

1
MSET {user:1}:name xiaolin {user:1}:age 18

在这个例子中,user:1 是参与哈希运算的部分,而 :name:age 不参与。因此两个 key 会算出相同的槽号,落入同一个节点,多 key 操作就可以正常执行了。

8.3 从节点默认不读写分离

在 Redis Cluster 中,从节点默认不提供读写服务。为什么不默认采取读写分离?主要是一致性问题。

主从复制是异步的,从节点的数据可能滞后于主节点。所以:

  • 强一致性数据(如库存、金额等)最好不要从从节点读取,否则可能引发业务问题。
  • 弱一致性数据(如商品详情页等不需要强一致的信息)可以采取读写分离,只需要最终一致即可。

如果需要开启从节点读取,客户端需要先向从节点发送一个 READONLY 命令:

1
READONLY

发送后,后续的读请求就会在从节点上正常处理,不会返回 MOVED 重定向到主节点。


九、为什么是 16384 个槽

最后回答一个常见的问题:为什么 Redis Cluster 的槽数量是 16384,而不是更多或更少?

9.1 网络开销:每次 PING 携带 2KB

集群中会进行大量的 PING 通信,信息传输速度必须快。在 PING 消息中,每个节点管理的槽范围是通过位图(bitmap)实现的,一个槽占一个 bit:

  • 管理的槽记为 1
  • 不管的槽记为 0

16384 bit ÷ 8 = 2048 字节 = 2 KB

每次 PING 携带 2KB 的位图信息,网络开销是可接受的。

9.2 负载均衡:1000 个节点每人约 16 个槽

为什么不选更小的数字(比如 8192)?因为 Redis Cluster 的建议规模上限是 1000 个主节点。

  • 16384 个槽 ÷ 1000 个节点 ≈ 每个节点 16 个槽,足以保证负载均衡。
  • 8192 个槽 ÷ 1000 个节点 ≈ 每个节点 8 个槽,略显紧张。

9.3 位运算优化:16384 是 2 的 14 次方

还有一个性能上的考量。计算槽位时使用的是位运算:

1
slot = CRC16(key) & 16383

因为 x % (2^n) 等价于 x & (2^n - 1),而 16384 = 2^14,16383 = 2^14 - 1。位运算 &% 取模运算更快。这也是为什么槽的数量要选一个 2 的幂次方。


十、总结

本文围绕 Redis Cluster 的六大核心问题展开,可以串联成一条完整的主线:

  1. 容量瓶颈:主从+哨兵解决了读写扩展和故障转移,但单机内存有上限,数据量超过一定阈值后 fork 和 RDB 生成会拖慢主线程。
  2. 数据分片:通过 16384 个哈希槽实现 Key 到槽的固定映射,再由槽映射到节点。扩缩容只需调整槽与节点的映射,不影响 Key 的归属。
  3. 客户端路由:客户端本地缓存槽与节点的映射表,直接计算 key 的槽号并发送到对应节点,无需代理层。
  4. 不停机扩容:通过 MOVED(迁移完成,更新映射表)和 ASK(迁移中,临时重定向)实现平滑扩缩容。Redis 7.0 通过槽位索引结构优化了迁移效率。
  5. 节点通信:采用去中心化的 Gossip 协议(PING/PONG/MEET/FAIL),避免中心化管理带来的运维复杂度和单点瓶颈。
  6. 故障转移:通过 PFAIL 本本记录实现客观下线判定,再通过类似 Raft 的投票机制选出数据最新的从节点升主。
  7. 多 key 处理:通过 Hash Tag 让多个 key 落入同一槽位;从节点默认不读写分离,需要时通过 READONLY 开启。

用一句话概括:Redis Cluster 通过哈希槽分片突破单机容量限制,通过 MOVED/ASK 重定向实现不停机扩容,通过 Gossip 协议实现去中心化通信,通过类 Raft 投票实现自动故障转移,最终在性能、容量和可用性之间找到了平衡。