一个 Redis Primary(主节点)可以保存很多数据,也可以处理很高的请求量,但它终究只有一份可写内存、一组 CPU 和一条主要的网络路径。增加 Replica(副本)能够提高读取能力,并在 Primary 故障后提供候选 Replica,却不会把一份可写数据集自动拆到多台机器上。

当数据量超过单台服务器的内存预算,或者独立请求的总负载超过一个 Primary 能够稳定承担的范围时,系统需要解决一个新的问题:

如何把不同的 key 分散到多个 Primary,同时让客户端找到正确节点,并在扩缩容和节点故障期间继续工作?

这正是 Redis Cluster 解决的问题。

本文是 Redis 分布式架构系列的第二篇。第一篇《Redis 高可用原理:Primary-Replica 复制、Sentinel 与故障转移》讨论了复制、自动故障转移以及异步复制的数据安全边界。本文继续使用同一个“光光平台”作为贯穿案例:平台的 PostgreSQL 保存文章和用户的业务事实,Redis 负责缓存文章正文、统计数据和其他可重建状态。随着文章数量和访问量增长,单个 Redis Primary 逐渐成为容量和吞吐瓶颈。

本文沿着平台从单 Primary 走向分片集群的因果链展开:先判断增加 Replica 为什么不能解决容量和写入上限,再解释 key、哈希槽与节点之间的映射;在此基础上分析客户端如何路由请求,以及扩缩容和故障期间如何通过重定向、在线迁槽(resharding,即把 slots 从一个 Primary 迁到另一个 Primary)和 Replica 晋升继续工作;最后回到 key 设计、负载边界和 Cluster 选型。

全文讨论的是 Redis Open Source 的 Cluster 模型。托管服务的代理路由、跨地域 Active-Passive 和 Active-Active 属于下一篇的范围。

术语说明:本文在叙述角色时统一使用 Primary(主节点)和 Replica(副本)。Redis Cluster 官方规范、命令输出、配置项和源码字段仍可能使用 masterreplica,旧版本中还可能出现 slave;引用这些原始字面值时会保留原文,以便与实际协议和字段对应。

1. 增加 Replica 为什么不能突破单 Primary 上限

1.1 Replica 复制的是同一份数据

假设内容平台最初只有一个 Redis Primary A。所有 key 都保存在 Primary A 中,所有写命令也都由 Primary A 执行。为了避免 Primary A 故障后只能人工恢复,平台为它增加 Replica A1 和 Replica A2。

Replica A1 和 Replica A2 保存的是 Primary A 数据集的副本。它们可以在允许读取旧数据时分担读请求,也可以在 Primary A 故障后被提升为新的 Primary。

但三个节点并没有共同提供三倍的可写内存。Primary A 保存 60 GB 数据时,每个 Replica 通常也需要保存相应的数据,而不是各保存其中 20 GB。增加 Replica 提高的是冗余和可选的读取能力,不是可写数据容量。

写入仍然集中在 Primary A。即使应用把部分只读请求发送到 Replica,SETINCRHSET 等写操作仍由当前 Primary 处理,再异步复制到 A1 和 A2。Replica 不能把一个 Primary 的写命令自动分摊成多个相互独立的可写数据集。

1.2 单 Primary 存在三类扩展上限

内容平台继续增长后,可能先后遇到三类问题:

上限 具体表现 增加普通 Replica 能否直接解决
内存容量 全部数据及运行开销无法稳定放入单个 Primary 的内存预算 不能,Replica 仍复制同一数据集
写入与命令执行能力 独立 key 的写入、计算或慢命令竞争同一 Primary 的资源 不能,写入仍进入 Primary
网络吞吐 一个 Primary 的入站和出站流量成为瓶颈 只能分担部分允许旧读的流量,不能分散 Primary 写流量

这里不能把“Redis 很快”理解为“单实例没有上限”。Redis 的实际能力取决于命令复杂度、value 大小、连接数、网络、持久化、复制和硬件。一个持续执行小型 GET 的实例,与频繁处理大集合运算、big key(大 key,即 value 或集合规模异常大的 key)或大量写入的实例,容量边界完全不同。

当瓶颈来自一个 hot key(热 key,即访问量高度集中在单个 key)时,分片也不会自动解决问题,因为一个 key 在任一时刻只属于一个 slot,并由一个 Primary 处理。Redis Cluster 更适合把大量彼此独立的 key 分散出去。

1.3 分片改变的是数据所有权

要突破单 Primary 的数据和写入上限,平台需要把 key space 划成多个部分:

Primary A:保存一部分文章 key
Primary B:保存一部分文章 key
Primary C:保存一部分文章 key

每个 Primary 只负责自己拥有的数据,独立 key 的请求可以在不同 Primary 上并行执行。此时增加 Primary 才可能同时增加:

  • 可用数据容量;
  • 独立请求的总处理能力;
  • 集群可使用的总网络带宽。

问题随之从“Primary 有没有 Replica”变为三个新问题:

  1. 一个 key 应该属于哪个分片?
  2. 客户端如何找到负责该 key 的节点?
  3. 增加或移除节点时,如何避免重新搬迁几乎全部数据?

Redis Cluster 使用固定数量的哈希槽作为这三个问题之间的中间层。

Redis 复制与数据分片的架构差异

2. Redis 为什么在 key 与节点之间加入固定哈希槽

2.1 直接对节点数量取模会放大拓扑变化

最直接的客户端分片方式,是先对 key 做哈希,再对节点数量取模:

node_index = hash(key) mod node_count

三个节点时,一个 key 可能映射到:

hash(article:8848:body) mod 3 = 1

如果增加第四个节点,计算变成:

hash(article:8848:body) mod 4

分母从 3 变成 4 后,大量 key 的结果都会变化。扩容本来只想让新节点接收一部分数据,却可能导致多数 key 重新映射,产生大规模数据搬迁和缓存失效。

一致性哈希常用一个逻辑哈希环缓解这个问题:节点和 key 都映射到环上,节点变化时主要影响相邻区间。它是理解分布式缓存分片的重要模型,但 Redis Cluster 并没有直接采用一致性哈希环。

本文只把一致性哈希作为对照。Redis Cluster 的关键选择是:不让 key 直接映射到当前节点,而是在二者之间加入一层数量固定的逻辑分区。

2.2 key 先映射到 slot,slot 再归属节点

Redis Cluster 把整个 key space 固定划分为 16384 个哈希槽,编号范围是 016383

普通 key 的 slot 计算规则是:

HASH_SLOT = CRC16(key) mod 16384

这里的 CRC16 使用 Redis Cluster 规范定义的 XMODEM 参数。16384 = 2^14,因此最终使用 CRC16 结果中的 14 位。

映射被拆成了两步。第一步只由 key 决定:只要 key 名不变,它计算出的 slot 就不变。第二步由集群拓扑决定:扩容时可以把若干 slots 从旧节点迁往新节点,而不需要改变其他 slots 的归属。

2.3 slot 是迁移和管理单位

假设内容平台有三个 Primary,可以把 slots 分成三组:

Primary A:    0 - 5460
Primary B: 5461 - 10922
Primary C:10923 - 16383

这只是便于理解的连续区间示例。一个节点负责的是一组 slot,slot 内则可能包含很多 key。例如,按 Cluster 规范计算,案例中的正文 key 落在 Primary B 负责的 slot 9232:

slot 9232
└── article:8848:body

如果加入 Primary D,集群不需要重新定义 HASH_SLOT 公式。运维工具只需从 A、B、C 中选择一部分 slots,将这些 slots 及其中的 key 迁往 D。扩容前后 slot 总数始终是 16384,变化的是 slot owner。

Redis Cluster 中 key 到 slot 再到 Primary 的两级映射

这层间接映射带来两个重要结果:

  • 迁移范围可控:扩容只搬迁被选中的 slots。
  • 客户端映射紧凑:客户端缓存的是 slot 到节点的关系,不需要缓存每个 key 的位置。

16384 个 slots 也构成 Primary 数量的理论上限,因为稳定状态下一个 slot 只由一个 Primary 负责。不过 Redis Cluster 规范建议实际节点规模大约控制在一千个节点量级,而不是追求 16384 个 Primary。

2.4 slot 均匀不代表负载一定均匀

CRC16 能让大量不同 key 较均匀地分散到 slots,但 slot 数量均匀不等于资源消耗均匀。一个节点负责的 slots 中可能恰好存在访问量极高的文章,另一个节点的 key 不多却包含几个很大的集合,还有一些节点保存了大量很小且很少访问的 value。

因此在线迁槽不能只比较 slot 数量,还要结合内存、CPU、网络、延迟和具体 key 的资源消耗判断。哈希槽提供了可移动的管理单位,但不会自动理解业务负载;第 6 章会继续讨论这种边界。

3. Cluster-aware client 如何把请求发到正确节点

3.1 客户端如何发现并直连目标节点

Redis Open Source Cluster 的节点不会把普通客户端命令代理到正确节点。客户端把请求发错位置时,当前节点返回重定向错误,由客户端重新发送。

这种设计避免所有请求都经过一个中心代理:

错误理解:
Client → 任意 Redis 节点 → 内部转发 → 目标节点

Redis Open Source Cluster:
Client → 目标节点
       ↘ 发错时收到重定向,再自行重试

稳定运行时,成熟客户端通常直接访问正确节点,请求仍可以只需要一次网络往返。

代价是客户端必须理解 Cluster 协议。只会连接单个固定地址、又不会处理 slot map 和重定向的普通客户端,不能正确使用 Redis Open Source Cluster。

应用配置的一个或多个 Redis 地址通常是 startup nodes,也就是发现集群的入口。它们不是永久 leader,也不是所有业务请求必须经过的网关。

Cluster-aware client(能够缓存 slot 到节点映射并处理 MOVED / ASK 重定向的 Cluster 客户端)启动后会获取类似以下信息:

0 - 5460       → Primary A
5461 - 10922   → Primary B
10923 - 16383  → Primary C

Redis 7.0+ 推荐客户端使用 CLUSTER SHARDS 获取 shard、节点角色(官方输出值为 master / replica)和 slot ranges 等拓扑信息。更早的服务端不支持该命令,只能使用 CLUSTER SLOTS;部分旧客户端也可能继续使用 CLUSTER SLOTS,但当前官方文档已将它标记为 deprecated。

客户端据此建立 slot map,并通常为需要访问的节点维护连接或连接池:

slotMap[0..5460]       = connectionPoolA
slotMap[5461..10922]   = connectionPoolB
slotMap[10923..16383]  = connectionPoolC

配置多个 startup nodes 的意义,是避免应用启动时恰好只配置了一个不可达入口。业务请求建立后,客户端会直接连接实际负责 slots 的节点。

3.2 一次正常请求的完整路径

article:8848:body 计算后属于 slot 9232,而 slot 9232 当前由 Primary B 负责。客户端查询本地 slot map,从 Primary B 的连接池取得连接并发送 GET;Primary B 执行命令并返回结果,节点 A 和 C 不参与这次请求,也不需要协调或合并结果。

Redis Cluster-aware client 从发现拓扑到直接路由请求的流程

这种方式是 Redis Cluster 能够横向扩展的重要原因:大量属于不同 slots 的单 key 请求,可以直接进入不同 Primary。

以上是默认的 Primary 请求路径。客户端若在某个 Replica 连接上显式发送 READONLY,可以从该 Replica 读取其 Primary 所负责 slots 的数据来扩展读;代价是可能读到异步复制产生的旧数据,而且写请求仍必须发往 Primary。

3.3 slot map 允许短暂过期

扩容、缩容、resharding 或故障转移都会改变 slot owner。客户端不可能在每次拓扑变化发生的同一瞬间全部更新。

Redis Cluster 的协议允许客户端暂时持有旧 map:

客户端认为:slot 9232 → Primary A
实际状态:  slot 9232 → Primary B

客户端先访问 A,A 再用重定向告诉客户端正确位置。客户端重试后刷新本地映射,系统最终恢复到直接路由。

因此,重定向不是异常设计的补丁,而是 Redis Cluster 正常的拓扑收敛机制。客户端必须限制重试次数、识别重定向环路,并在多次拓扑变化后重新拉取完整 slot map,不能无限重试掩盖错误配置。

4. MOVEDASK 分别表示什么

MOVEDASK 都会通过重定向响应告诉客户端下一步应访问的目标,但二者表达的时间语义完全不同:

重定向 含义 客户端是否更新 slot 的永久映射
MOVED 当前节点认为该 slot 已由另一个节点正式负责 应更新;通常重新获取完整拓扑
ASK slot 正在迁移,这一次请求需要临时访问目标节点 不应更新;只重定向下一次请求

两类重定向的目标都使用 endpoint:port。endpoint 可以是 IP、hostname,也可以为空。例如 -MOVED 3999 :6380-ASK 3999 :6380 都表示目标节点没有可通告的 endpoint;客户端应沿用当前请求使用的 endpoint,只替换为响应给出的端口。自研客户端不能把 MOVEDASK 的 host 部分假设为永远非空。

理解这个区别,是理解在线 resharding 的关键。

其中 ASK 主要出现在传统的逐 key 迁槽流程。Redis 8.4+ 的 Atomic Slot Migration 在复制阶段让客户端继续访问源节点,只在原子切换所有权后通过 MOVED 引导客户端;但客户端仍应正确处理 ASK,以兼容旧版本或仍使用传统迁槽工具的环境。

Redis Cluster MOVED 与传统迁槽中 ASK 重定向的时间语义对比

4.1 MOVED 表示正式所有权

如果客户端把 slot 9232 的请求发给错误节点,可能收到:

-MOVED 9232 10.0.0.12:6379

错误中包含:

  • slot 编号 9232
  • 当前能够服务该 slot 的 endpoint:port

客户端需要把原命令重新发送到 10.0.0.12:6379

收到 MOVED 后,只修改 slot 9232 的映射在协议上可以工作,但官方规范建议考虑重新获取完整 slot map。原因是一次拓扑变化往往影响多个 slots:

  • resharding 可能一次移动很多 slots;
  • Replica 晋升后,原 Primary 的全部 slots 都会映射到新 Primary;
  • 客户端当前连接的节点也可能持有短暂过期的信息。

完整刷新可以减少后续连续遇到 MOVED 的次数。

4.2 ASK 为什么必须配合 ASKING

slot 迁移时,部分 key 已经到达目标节点,另一些 key 仍在源节点。此时不能把客户端的永久映射过早改成目标节点。

如果源节点发现请求的 key 已不在本地,并且该 slot 正在迁往目标节点,它会返回:

-ASK 9232 10.0.0.14:6379

客户端应在目标节点的同一连接上依次发送:

ASKING
GET article:8848:body

ASKING 为这条连接设置一次性标记,让处于 IMPORTING 状态的目标节点服务紧随其后的那条命令。请求完成后,客户端仍然保留:

slot 9232 → 源节点

下一次访问 slot 9232 时,客户端仍先访问源节点。只有迁移完成、slot 正式归属目标节点后,源节点才会返回 MOVED,客户端再永久更新映射。

迁移期间,集群对外公布的正式 owner 仍然是源节点。如果目标节点无条件接受该 slot 的所有请求,持有错误拓扑的客户端就可能绕过源节点,在目标节点提前创建新 key,使数据位置更难收敛。

因此目标节点处于 IMPORTING 状态时:

  • 带有一次性 ASKING 标记的请求可以执行;
  • 普通请求仍按照正式 slot owner 返回 MOVED

这保证 ASK 只是迁移协议中的临时通道,而不是第二个长期可写 owner。

4.3 客户端的处理状态机

落到客户端实现时,MOVEDASK 必须进入不同分支:前者把原命令重试到目标节点并刷新 slot map,后者只在目标连接上通过 ASKING 重试当前命令,不修改永久映射。

此外,客户端还需要区别处理:

  • CROSSSLOT:key 设计不满足同 slot 要求,通常不是短暂重试能解决;
  • TRYAGAIN:resharding 中的临时多键状态,可以在有界等待后重试;
  • 连接失败或超时:需要重新选择可达节点,并可能刷新拓扑。

把所有错误都归为“重试一下”会掩盖永久的数据模型问题,也可能造成请求风暴。

5. Redis 如何在线完成 resharding

添加节点、移除节点和负载再平衡,最终都要移动 slots。Redis 8.4 之前的传统流程按批次迁移 key;Redis 8.4+ 新增 Atomic Slot Migration(ASM,原子哈希槽迁移),可以复制整个 slot 的快照和持续写入,再一次性切换所有权。两种机制的请求语义和风险边界不同,本章分别说明。

假设平台新增 Primary D,并计划把 slot 15140 从 Primary C 迁到 D。其中 {8848} 是 hash tag(哈希标签):Redis 只对花括号内的 8848 计算 slot,因此 article:{8848}:bodyarticle:{8848}:stats 都属于 slot 15140。第 6 章再讨论这种 key 设计的适用边界。

5.1 传统迁槽如何建立两侧状态

在 Redis 8.4 之前,或仍使用传统迁槽工具时,运维工具先把目标 D 标记为 IMPORTING,再把源 C 标记为 MIGRATING

在目标 D:
CLUSTER SETSLOT 15140 IMPORTING <Primary-C-node-id>

在源 C:
CLUSTER SETSLOT 15140 MIGRATING <Primary-D-node-id>

正式 owner 此时仍是 C。key 还在 C 时由 C 执行;key 已迁走或本地不存在时,C 返回 ASK。D 只接受带一次性 ASKING 标记的请求,普通请求仍被 MOVED 回 C。这样可以逐批迁移 key,而不会提前形成两个长期可写 owner。

5.2 传统迁槽如何逐批移动并验证清空

管理工具需要重复执行“取一批、迁一批、再计数”的循环,而不是调用一次 MIGRATE 就切换 owner:

重复执行:
CLUSTER GETKEYSINSLOT 15140 <count>
MIGRATE <target-host> <target-port> "" 0 <timeout> KEYS <key...>

直到源节点返回:
CLUSTER COUNTKEYSINSLOT 15140
(integer) 0

MIGRATE ... KEYS 的批量模式只是把多个 key 的 RESTORE 请求放入 pipeline,以减少网络往返,并不会把整批 key 包进一个事务。目标每确认一个 key 恢复成功,源端才删除对应 key;因此原子边界是每个已获目标确认的 key,而不是整个批次,更不是整个 slot。某些 key 成功、另一些 key 失败时,同一次批量调用可以只完成一部分;MIGRATE 仍是阻塞命令,批次过大或包含 big key 都会放大停顿。

错误和超时都需要按可部分完成的结果处理。普通目标错误可能让已确认的 key 位于目标、失败的 key 仍留在源端;如果目标已完成 RESTORE,但源端在收到确认或执行 DEL 前发生超时、I/O 错误,同一个 key 还可能暂时同时存在于两侧。工具必须核对源、目标的实际状态,安全重试,并重新确认源端 CLUSTER COUNTKEYSINSLOT 最终为 0,不能把失败响应直接解释成“什么都没发生”。

5.3 传统迁槽何时才能切换 owner

只有确认源节点的 CLUSTER COUNTKEYSINSLOT 15140 返回 0 后,才能把 slot 正式绑定到 D。结束迁槽时必须先通知目标 D,再通知源 C:

在目标 D:
CLUSTER SETSLOT 15140 NODE <Primary-D-node-id>

在源 C:
CLUSTER SETSLOT 15140 NODE <Primary-D-node-id>

可选,在其他 Primary:
CLUSTER SETSLOT 15140 NODE <Primary-D-node-id>

目标 D 在 IMPORTING 状态下把 slot 绑定给自己时,会清除导入状态,并确保自身拥有能够覆盖旧归属声明的 config epoch;如果它还没有集群中最大的 config epoch,Redis 会为它创建一个新的。D 负责把新 owner 传播给集群,然后源 C 才清除 MIGRATING 状态并放弃所有权。这个顺序不能颠倒:如果先让 C 放弃 slot,而 D 在成为正式 owner 前故障,slot 可能处于没有 owner 的状态。通知其他 Primary 不是协议必需步骤,但可以让它们更快停止返回旧地址,减少额外重定向。

新配置传播后,仍持有旧 map 的客户端访问 C 时会收到指向 D 的 MOVED。此时 ASK 阶段结束,后续请求逐步恢复为直接访问 D。

5.4 Redis 8.4+ 的 ASM 如何原子切换 slot

Redis 8.4+ 可以在目标 Primary 上发起 CLUSTER MIGRATION

CLUSTER MIGRATION IMPORT 15140 15140
CLUSTER MIGRATION STATUS ALL

IMPORT 返回任务 ID。目标 D 随后连接源 C,接收 slot 快照和迁移期间的持续写入;复制阶段客户端继续访问 C,不会因为部分 key 已移动而进入连续的 ASKTRYAGAIN 路径。

当快照完成且增量写入的剩余量降到阈值以下时,C 会短暂暂停写入,把尾部变更交给 D。D 应用完变更后一次性接管 slot 所有权,并通过 Cluster Bus(节点间传播拓扑和故障状态的内部通信通道)发布新配置;C 恢复写入,客户端收到 MOVED 后转向 D,源节点随后进入旧 slot 数据清理。Redis 通常可以整 slot 分离并交给后台线程异步释放;模块不支持 per-slot 数据结构或启用 CLIENT TRACKING 时,则会退回主线程 cron 中的增量清理。

这里暂停的是源节点 C 的写入,而不只是正在迁移 slot 的写入。cluster-slot-migration-write-pause-timeout 默认是 10 秒;如果 D 没能在超时前完成接管,C 会把迁移视为失败并恢复写入。该参数不只是延迟上限:如果设置过低,C 恢复写入后 D 又迟到发布了新 owner 配置,这段没有继续复制到 D 的写入可能丢失。因此不能为了缩短暂停机械调低它,变更前应在目标负载和故障场景下验证超时、恢复写入与配置发布的竞态。

ASM 中的“原子”指 slot 所有权和对外服务位置一次性切换,不是把迁移期间的业务命令变成跨节点强一致事务。任务成功时 STATUSstatecompleted;运维侧仍要检查失败、重试、写暂停时长,以及数据清理阶段对尾延迟的影响。

Redis Cluster 传统逐 key 迁槽与 Redis 8.4+ ASM 生命周期对比

5.5 “在线迁移”不等于“没有影响”

两种机制都不要求停掉整个集群,但“在线”不等于没有资源成本:

机制 客户端在复制期间看到什么 主要影响
传统逐 key 迁槽 已迁 key 触发 ASK;同 slot 多键操作可能 TRYAGAIN MIGRATE 阻塞、重定向与重试、big key 延迟
Redis 8.4+ ASM 继续访问源节点,所有权切换后收到 MOVED slot 快照与增量流消耗资源,切换时存在短暂写暂停

生产迁槽仍应控制并发范围,避开业务高峰,观察 CPU、网络、内存、尾延迟、任务状态和客户端错误。移除 Primary 前还必须先迁出全部 slots,验证其不再拥有数据,再把节点移出集群。

6. 分片如何改变 key 设计与负载边界

分片把请求分散到多个 Primary,也打破了单实例中“任意 key 都在同一个进程内”的前提。应用的数据模型必须显式面对 key 的位置。

6.1 多键操作为什么要求同一个 slot

假设平台最初使用两个 key:

article:8848:body
article:8848:stats

它们会分别对完整 key 计算 CRC16,因此通常不能假定落在同一个 slot。

如果应用执行:

MGET article:8848:body article:8848:stats

而两个 key 属于不同 slots,Redis Cluster 会拒绝请求:

-CROSSSLOT Keys in request don't hash to the same slot

这是 Redis Cluster 的数据模型约束,不是一次短暂网络故障。盲目重试同一命令不会让 key 自动移动到一起。

同 slot 约束不仅影响 MGETMSET 等显式多键命令,也影响:

  • 一个事务中的全部 key;
  • Lua 脚本声明和访问的 keys;
  • 集合交集、并集和差集等多键运算;
  • 重命名或复制等同时涉及源 key 与目标 key 的操作。

应用在迁移到 Cluster 前,需要盘点命令和脚本实际涉及的全部 keys,而不是只检查最常见的 GETSET

6.2 hash tag 如何让相关 key 落入同一 slot

Redis Cluster 提供 hash tag。如果 key 中第一个 { 右侧的第一个 } 之间包含至少一个字符,Redis 只对二者之间的内容计算 slot;如果没有配对的 },或二者之间为空,则仍对完整 key 计算 slot。

平台可以把 key 改成:

article:{8848}:body
article:{8848}:stats

两者都只对 8848 计算哈希,因此一定属于同一个 slot。此时可以执行:

MGET article:{8848}:body article:{8848}:stats

hash tag 适合表达需要共同原子操作的数据局部性。例如把“同一篇文章的正文和统计状态”放在一起,而不是把所有文章都放在一起。

下面这种设计虽然也能保证同 slot,却会破坏分片:

{article}:8848:body
{article}:8848:stats
{article}:9001:body
{article}:9001:stats

因为所有 key 都使用相同 tag article,它们会集中到同一个 slot 和同一个 Primary。正确粒度通常应来自真正需要共同操作的业务实体,例如文章 ID、用户 ID 或订单 ID。

6.3 CROSSSLOTTRYAGAIN 不是同一种失败

CROSSSLOT 通常表示请求中的 keys 从设计上属于不同 slots:

key A → slot 100
key B → slot 9000

修复方向是调整 key 设计、拆分操作,或使用合适的 hash tag。

TRYAGAIN 则可能发生在传统逐 key 迁槽时。即使多个 keys 计算到同一个 slot,它们也可能暂时分布在源和目标两侧:

同一个 slot 15140:
article:{8848}:body  → 已迁到 D
article:{8848}:stats → 仍留在 C

Redis Cluster 规范规定:

  • 多个 keys 都存在且都仍位于源节点或都已位于目标节点时,操作仍可执行;
  • keys 分散在两侧,或操作涉及迁移期间不存在的 key 时,可能返回 TRYAGAIN

客户端可以对 TRYAGAIN 做有界、带退避的重试;迁移结束后,同 slot 多键操作恢复正常。它不能被当作无限重试的理由,更不能与永久性的 CROSSSLOT 混为一谈。Redis 8.4+ 的 ASM 在复制阶段继续由源节点服务请求,避免把部分 key 提前暴露在目标节点,因此不再具有上面这种中间状态。

6.4 hot key 不会被一个 slot 自动拆开

一个 key 在任一时刻只属于一个 slot,一个 slot 在稳定状态下只由一个 Primary 负责。

如果首页每次访问都读取:

article:{homepage}:feed

即使集群从 3 个 Primary 扩到 30 个,这个 key 的请求仍集中在负责它的一个 Primary。Cluster 能分散很多独立 key,却不会把一个 String、Hash、List 或 Sorted Set 的内部元素自动拆到多个 Primary。

hot key 的典型信号是:

  • 某一个 shard CPU 或网络明显高于其他 shards;
  • slot 数量和 key 数量看似均匀,但延迟集中在一个节点;
  • 某个 key 的访问频率远高于其他 key。

处理方式取决于业务语义:

  • 对可接受短暂旧值的只读热点增加应用本地缓存;
  • 将可分割的数据按更细业务维度拆成多个 keys;
  • 减少重复读取或合并上游请求;
  • 对无法拆分的写热点进行限流或重新设计聚合方式。

拆分 key 会改变原子性和读取方式,不能只为均衡负载机械加随机后缀。

6.5 big key 会放大迁移和故障影响

big key 指 value 本身很大,而不是 key 名很长。例如:

  • 包含大量字段的 Hash;
  • 含有大量元素的 List、Set 或 Sorted Set;
  • 长时间未裁剪的 Stream;
  • 很大的 String。

big key 会带来多重影响:

  • 命令处理和删除可能占用更长时间;
  • 网络传输占用更多带宽;
  • 传统 MIGRATE 搬迁它时,源和目标节点的阻塞窗口更长;
  • 对较大的非模块数据类型 key(例如大型 Hash),ASM 会切换为类似 AOF 的分块格式,以降低峰值内存;Redis 模块数据类型等其他情况不能假定具有相同的分块行为,而且 slot 快照和增量复制仍会消耗网络、CPU 与内存;
  • Replica 同步和故障恢复的成本更高。

因此 Cluster 扩容前,应同时识别 hot key、big key 和 slot 倾斜。仅看到“还有很多空 slots”并不能证明迁移成本很低。

Redis CLI 提供 --memkeys--bigkeys--keystats 等采样能力。较新 Redis 版本还提供 hot key 与 slot 统计相关命令。具体工具能力与开销随版本变化,生产使用前应核对目标版本文档并控制采样影响。

7. Cluster 如何检测故障并提升分片 Replica

Redis Cluster 不只把数据分片。每个 Primary 还可以配置一个或多个 Replica,在某个分片 Primary 故障后接管它负责的 slots。

典型六节点布局是:

Primary A ── Replica A1
Primary B ── Replica B1
Primary C ── Replica C1

A、B、C 共同覆盖 16384 个 slots,A1、B1、C1 分别复制对应 Primary。Replica 用于高可用,不会额外拥有一组独立可写 slots。

7.1 Cluster Bus 与 gossip 如何传播状态

Redis Cluster 节点之间通过 Cluster Bus 通信。节点会交换心跳和 gossip 信息;gossip 是让节点把自己知道的状态继续传播给其他节点,使各自维护的拓扑视图逐步收敛。传播的信息包括:

  • 已知节点及其角色;
  • 节点是否可达;
  • Primary 负责的 slots;
  • Replica 当前复制哪个 Primary;
  • 集群配置 epoch(用于比较 slot 所有权声明新旧的单调逻辑版本号);
  • 从当前节点视角看到的集群状态。

每个节点都维护自己的拓扑视图。这些视图通过持续通信最终收敛,而不是依赖一个中心元数据服务器。

客户端通信端口与 Cluster Bus 是不同用途的网络通道。节点间无法正确通信时,即使应用还能连到个别 Redis 端口,故障检测、配置传播和自动 failover 也可能无法正常工作。

7.2 从 PFAIL 到 Replica 晋升

下文的“Primary 多数派”均按 cluster_size 计算:官方字段定义统计至少服务一个 slot 的 master,本文统一称为 Primary。刚加入但尚未分配 slot 的空 Primary 不计入多数派分母。

当一个节点向另一节点发出的 active PING 持续超过 NODE_TIMEOUT 仍未收到响应时,会在本地把对方标记为 PFAIL,即 possible failure。

PFAIL 只是当前节点的判断:

Primary A 认为 Primary B 不可达
≠
整个集群已经确认 Primary B 故障

如果只有 A 到 B 的局部网络链路异常,而 Primary 多数派仍能访问 B,立即提升 B 的 Replica 可能制造不必要的角色冲突。

节点会通过 gossip 收集其他 Primary 对 B 的看法。当有效时间窗口内有 Primary 多数派报告 B 处于 PFAILFAIL 状态时,B 才会被提升为 FAIL。检测到 FAIL 的节点还会向可达节点广播失败消息,加速状态传播。

这不是要求所有节点在同一时刻执行一次强一致投票,而是通过带有效期的故障报告形成多数确认。正式的 Replica 晋升还会执行单独的选举和多数授权。

一个 Replica 发起选举前,至少需要满足以下条件:

  • 它的 Primary 在自身视角已是 FAIL
  • 原 Primary 确实负责非空 slots;
  • Replica 与 Primary 断开连接的时间没有超过允许的新鲜度范围。

同一 Primary 的多个 Replica 可能都有资格参选。为了让数据较新的 Replica 更早尝试,Replica 会根据已处理的复制 offset 形成一个尽力而为的 rank:

复制进度更靠前的 Replica
    → 等待时间通常更短
    → 更早发起选举

这提高了较新 Replica 胜出的概率,但不是“永远选择零数据损失 Replica”的强保证。第一篇已经解释过,异步复制仍可能让已确认写入在 failover 时丢失。

候选 Replica 会递增当前 epoch,并向 Primary 请求授权。获得 Primary 多数派的 ACK 后,它才能赢得选举。

Replica 获得多数授权后,会:

  1. 把自己切换为 Primary;
  2. 取得一个新的、更大的配置 epoch;
  3. 宣告自己负责原 Primary 的 slots;
  4. 向其他节点广播新配置。

其他节点看到同一 slots 的冲突声明时,会使用更大的配置 epoch 判断哪份配置更新。最终,slot map 收敛到新 Primary:

故障前:
slot 5461 - 10922 → Primary B

故障后:
slot 5461 - 10922 → Replica B1 晋升后的新 Primary B1

客户端持有旧 map 时,通常会先遇到连接失败并刷新拓扑;如果请求被重试到其他可达但并不负责该 slot 的节点,也可能收到指向 B1 的 MOVED。两条路径最终都会让客户端转向 B1。

Redis Cluster 从 PFAIL、多数确认、Replica 选举到接管 slots 的故障转移流程

旧 Primary B 恢复并重新加入集群后,会获知这些 slots 已被更高 epoch 的 B1 接管。如果它原先只负责这些 slots,最终会转为 B1 的 Replica,而不是继续作为另一个可写 Primary。

7.3 多数派和 full coverage 决定可用范围

Redis Cluster 在网络分区的少数派一侧不可用。多数派一侧要恢复完整服务,需要:

  • Primary 多数派仍然可以相互通信;
  • 每个不可达 Primary 都有一个可达且可晋升的 Replica。

如果 Primary B 和唯一的 Replica B1 同时失效,B 原来负责的 slots 就没有可用 Replica。

Redis Cluster 的两个配置会影响此时的服务边界:

  • cluster-require-full-coverage yes:默认要求全部 slots 都由可用节点覆盖;存在未分配或 owner 已是 FAIL 的 slot 时,把集群标记为 down。down 状态下写入会被拒绝,是否继续允许读取由下一项配置决定。
  • cluster-allow-reads-when-down no:默认在集群被标记为 down 时也不继续提供普通读取,以避免从不了解最新拓扑的节点读取不一致数据。设为 yes 后,当前 owner Primary 可以读取自己仍拥有的 slots;如果 Replica 连接已显式发送 READONLY,并且该 slot 当前映射的 owner 仍是它的 Primary,也可以从该 Replica 读取。两条路径的结果都可能陈旧,写入仍被拒绝。

cluster-require-full-coverage 设为 no 后,在当前节点仍能联系按 cluster_size 计算的 Primary 多数派的前提下,由可达、健康 owner 服务的 slots 可以继续处理读写;未被可用节点覆盖的 slots 仍然不可用,应用得到的是部分可用系统:

slot 0 - 5460       → 可用
slot 5461 - 10922   → 不可用
slot 10923 - 16383  → 可用

这并不天然优于全覆盖失败。应用必须能够识别哪些业务 key 不可用,避免把部分成功误认为完整成功。

还要把“slots 不完整”和“失去 Primary 多数派”区分开。cluster-require-full-coverage no 只放宽前一种情况,不能绕过网络分区下的写保护:

集群状态 配置的作用 可用边界
少数 slots 未被可用节点覆盖,但节点仍能联系 Primary 多数派 cluster-require-full-coverage no 允许其余由可达、健康 owner 服务的 slots 继续服务 只有未受影响的 slots 可读写
当前节点无法联系 Primary 多数派 无论 full coverage 如何设置,节点都会进入 down 状态 写入仍被拒绝
集群已经 down cluster-allow-reads-when-down yes 可以放行 owner Primary 的读取,也可以放行已发送 READONLY、且其 Primary 仍是 slot owner 的 Replica 连接读取 只能读取,结果可能陈旧;不能写入

因此,full coverage 是“是否允许部分 key space 继续工作”的策略,不是让少数派继续写入的开关。

7.4 自动 failover 仍有一致性边界

Redis Cluster 使用异步复制。Primary 通常不会等待 Replica 确认每一条写入就向客户端返回。

因此可能发生:

客户端写入 Primary B
  → B 返回 OK
  → 写入尚未到达 B1
  → B 故障
  → B1 被提升
  → 已确认写入丢失

网络分区少数侧在 NODE_TIMEOUT 到期前还可能短暂接受写入。如果多数侧随后完成 failover,这些写入也可能在配置收敛时丢失。

Redis Cluster 的目标是高性能、横向扩展和尽力保留写入,不是提供强一致共识。WAIT 可以降低写入尚未到达足够 Replica 的概率,但不能把 Cluster 变成强一致系统。

对于余额、账本、订单最终状态等不能接受已确认写入丢失的数据,仍应由具备相应事务与持久性保证的数据库作为事实来源。Redis 更适合保存缓存、派生状态或能够通过业务事实重建的数据。

8. Redis Cluster 的选型检查、架构演进与总结

Redis Cluster 是为横向扩展设计的,不是每个生产 Redis 的默认答案。小型数据集为了“看起来更高可用”直接上 Cluster,可能只增加客户端、键设计和运维复杂度,却没有解决实际瓶颈。

8.1 采用前检查:必要性、数据模型、拓扑与运维

先确认问题是否真的需要分片。

可以从以下问题开始:

问题 如果答案是“是” Cluster 的作用
数据和必要运行开销是否无法稳定放入一个 Primary? 需要把 key space 分到多个 Primary 扩展总内存容量
大量独立 key 的写入或命令是否受单 Primary 的 CPU、网络限制? 需要把独立请求分散执行 扩展总处理能力
瓶颈是否只是读取,并且业务允许旧读? 可能先增加 Replica Cluster 不一定是第一选择
瓶颈是否来自单个 hot key 或 big key? 需要先修改访问或数据模型 Cluster 不会自动拆分单 key
是否只需要自动故障转移,不需要分片? 非分片 HA 可能更简单 优先评估第一篇的方案

采用 Cluster 前应有目标环境的容量和负载证据,而不是套用固定 QPS 或固定数据量阈值。

检查应用是否满足 Cluster 的数据模型。

上线前至少检查:

  • 客户端是否真正支持 Redis Cluster;
  • 客户端是否正确处理 MOVEDASK、连接失败和拓扑刷新;
  • multi-key 命令涉及的 keys 是否同 slot;
  • 事务和 Lua 脚本是否声明并访问同 slot keys;
  • hash tag 是否按业务实体设计,而不是把全部数据集中到一个 tag;
  • 是否存在无法拆分的 hot key;
  • 是否存在会放大迁移阻塞的 big key;
  • 应用是否错误依赖 SELECT,因为 Redis Cluster 只支持数据库 0

这些问题中,很多无法在部署 Cluster 后靠增加节点补救。key 命名和原子操作边界应在迁移前确定。

检查集群拓扑是否具备实际高可用能力。

Redis 官方教程指出,能按预期工作的最小 Cluster 至少需要三个 Primary;生产环境强烈建议从六节点布局开始,即三个 Primary 和三个 Replica。

节点数量本身还不够。还需要确认:

  • 每个 Primary 是否有可晋升 Replica;
  • Primary 与对应 Replica 是否位于独立故障域;
  • cluster_size 计算的 Primary 多数派能否在预期网络故障下继续通信;
  • Cluster Bus 和客户端网络是否正确开放;
  • 节点地址是否能被客户端实际访问;
  • NODE_TIMEOUT、full coverage 和读取策略是否符合可用性目标。

把六个 Redis 进程放在同一台物理机上,可以测试协议,却不能抵御主机故障。故障域布局比“进程数量看起来够多”更重要。

检查运维体系是否能管理动态拓扑。

Cluster 运行后需要持续观察:

  • cluster_state
  • 已分配、正常、PFAILFAIL 的 slot 数量;
  • 每个 Primary 的内存、CPU、网络和延迟;
  • Replica 状态与复制 lag;
  • MOVED、传统迁槽中的 ASK / TRYAGAIN 和永久性的 CROSSSLOT 错误;
  • Redis 8.4+ ASM 的任务状态、重试次数与写暂停时长;
  • slot、key 数量和实际资源消耗是否倾斜;
  • hot key、big key 和慢命令;
  • resharding 前后的全部 slot coverage。

还应在受控环境中验证:

  1. 一个 Primary 故障后 Replica 能否晋升;
  2. 客户端是否刷新 slot map 并恢复请求;
  3. 目标版本的迁槽流程中,单 key 请求是否持续工作;
  4. 使用传统迁槽时,多键请求如何处理 TRYAGAIN;使用 ASM 时,任务失败与写暂停是否符合预期;
  5. Primary 与 Replica 同时故障时应用如何降级;
  6. 备份是否覆盖全部 Primary,并能按集群边界恢复。

Cluster 的每个节点只持久化自己负责的数据。备份单个节点不等于备份整个集群;完整边界可参见《Redis 持久化原理:RDB、AOF、数据恢复与生产实践》中的 Cluster 备份章节。

8.2 回到内容平台:从单节点到分片集群

内容平台的演进可以总结为:

阶段一:单 Primary
  └── 简单,但容量、写入和可用性受单点限制

阶段二:Primary + Replica + 自动故障转移
  └── 解决节点接管,但总可写数据仍在一个 Primary

阶段三:三个 Primary shards + 每个 shard 的 Replica
  ├── 16384 slots 分配到三个 Primary
  ├── Cluster-aware client 直接访问目标 Primary
  ├── hash tag 保证同一文章的相关 key 同 slot
  ├── resharding 支持继续增加 Primary
  └── 分片 failover 处理单个 Primary 故障

这套架构解决了单区域内的横向扩展与分片级故障转移,但它仍没有回答:

  • 是否要让客户端直接感知所有 shards;
  • 是否应该使用稳定 endpoint 和服务端 proxy;
  • 整个区域故障后如何切换;
  • 多地域是否需要同时写;
  • 缓存、session、队列和控制状态是否应该共用同一 Redis。

这些问题将在系列第三篇《Redis 生产架构选型:自建、托管、代理与跨地域容灾》中讨论。

8.3 本文的八个结论

  1. Replica 复制同一份数据,能够提供冗余和可选读扩展,但不能突破单 Primary 的可写内存与写入上限。
  2. Redis Cluster 不直接使用一致性哈希,而是让 key 先映射到固定的 16384 个 slots,再把 slots 分配给 Primary。
  3. Cluster-aware client 缓存 slot map 并直接访问目标节点;Redis Open Source Cluster 没有中心节点代理普通请求。
  4. MOVED 指向 slot 的当前正式 owner,客户端应持久修正映射;ASK 表示传统迁槽期间下一条请求应临时访问目标节点。
  5. 传统迁槽通过 MIGRATINGIMPORTING 和逐批 MIGRATE 移动 key;Redis 8.4+ ASM 则复制 slot 快照与持续写入,再原子切换所有权。
  6. multi-key、事务和 Lua 脚本要求相关 keys 同 slot;hash tag 能提供局部性,也可能制造 hot slot。
  7. PFAIL 是单节点怀疑,FAIL 需要 Primary 多数派的有效故障报告;Replica 获得多数授权后才能晋升并接管 slots。
  8. Redis Cluster 解决分片扩展和实用高可用,不解决单个 hot key、强一致性、历史备份或跨地域流量切换。

参考资料