#DevOps

Redis Cluster:分片、高可用与故障转移

单 Redis 扛不住内存/吞吐时,Redis Cluster 水平扩展。本文讲清 Cluster 的分片模型(16384 槽位 + hash tag)、主从复制与自动故障转移、客户端路由(MOVED/ASK)、跨槽操作限制,以及什么时候该上 Cluster(>单机内存/高吞吐)什么时候用哨兵就够。

本文要点

  • 主题聚焦:DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年2月6日·更新于 2026年7月23日

我把 Redis Cluster 拆开讲过好几轮,每次都有人问同一个问题:"我们一个主从加哨兵跑得好好的,为什么要上 Cluster?" 多数时候答案是不用上。但单机内存撑爆、单核写吞吐打满、或者主从全量同步动辄几十 GB 半天同步不完的时候,Cluster 就不是"要不要"的问题,而是"怎么上得稳"的问题。

这篇把 Redis Cluster 的分片模型、主从与故障转移、客户端路由、跨槽限制、以及和 Sentinel 的选型边界一次讲透。配置按生产可用标准写,坑标在明面上。

一、单机 Redis 的天花板:什么时候必须上 Cluster

先把"什么时候不该上 Cluster"说清楚——这是更常见的情况。一个 Redis 进程跑得好好的,你硬要上 Cluster,徒增运维复杂度:客户端要支持集群协议、跨槽操作受限、批量命令不能用、迁移要精细操作。没有碰到下面这两道墙之前,Cluster 都是过度设计。

墙一:内存。 Redis 是纯内存的,数据集大小 ≈ 内存大小。单机内存有物理上限——一台 256GB 的机器,跑一个 Redis 实例,扛到 100GB+ 时 fork(RDB/AOF 重写)的延迟和 COW 内存翻倍就开始咬人(这块在 Redis 持久化 里详细讲过)。再往上堆,单实例就是一颗定时炸弹:重启恢复几十分钟、fork 卡秒级、OOM 被 kill。数据量超过单机舒适区(一般 30-50GB),分片就是唯一的出路。

墙二:单核吞吐。 Redis 主线程是单线程模型(6.0 之后网络 I/O 多线程,命令执行仍是单线程)。一个实例的写 QPS 天花板大致在 10-15 万,读好一些。当你一个实例的 QPS 持续顶到这个量级,CPU 跑满、延迟抖动,加从节点只能分担读,写仍然只能压在主节点一个核上。这时候要扩写吞吐,只能水平分片。

除了这两道墙,还有一个隐性诉求:故障爆炸半径。单实例宕了,所有 key 空间都不可用;分片后,一个分片挂了(在有从节点 failover 之前)只影响那一段 key。但这只是附带好处,不是上 Cluster 的主因——主因永远是"单机装不下了"或"单核写不动了"。

text
单 Redis 实例(含主从 + 哨兵)

         ┌─────────────────────────┐
 客户端 → │   Master (全量数据)      │ ← 单核写 QPS 上限 ~10-15 万
         │   内存上限 ≈ 单机物理内存 │ ← fork 延迟、COW 翻倍
         └─────────────────────────┘
              │ replication
         ┌────┴────┐
         ▼         ▼
      Replica   Replica        ← 只分担读,写压不住

Redis Cluster 的解法是:把 key 空间切成 16384 个槽(slot),分散到多个主节点,每个主节点再带从节点做高可用。写吞吐随分片数线性扩展,内存随节点数累加,单分片故障不影响其他分片。

text
Redis Cluster(3 主 3 从,槽位分散)

     ┌──────────────┐   ┌──────────────┐   ┌──────────────┐
     │  Master A    │   │  Master B    │   │  Master C    │
     │ 槽 0–5460    │   │ 槽 5461–10922│   │ 槽 10923–16383│
     └──────┬───────┘   └──────┬───────┘   └──────┬───────┘
            │ replication      │                  │
            ▼                  ▼                  ▼
       ┌────────┐         ┌────────┐         ┌────────┐
       │ReplicaA│         │ReplicaB│         │ReplicaC│
       └────────┘         └────────┘         └────────┘

   写吞吐 = 3 × 单核        内存总量 = 3 × 单机        ← 水平扩展

二、分片模型:16384 个槽位与 CRC16

Redis Cluster 的分片核心是一句话:slot = CRC16(key) % 16384。理解这一行,就理解了 Cluster 的数据分布。

几个关键点拆开看:

1. 为什么是 CRC16,不是取 key 的 hash 或一致性哈希? Redis Cluster 没有用一致性哈希(consistent hashing),用的是固定数量的槽(slot)+ 哈希取模。一致性哈希的优点是加减节点时数据迁移量小,但数据分布不均、范围查询困难。Redis 选择把 key 空间离散成 16384 个固定的槽,节点接管的是"槽区间",加减节点时迁移的是整个槽(而不是单个 key),这让迁移、路由、重定向都有了清晰的边界。

2. 为什么是 16384,不是 65536 或更大? 这是 Redis 作者 Salvatore 在邮件列表里专门回答过的。原因有三个:

  • Gossip 带宽。集群节点之间通过 Gossip 协议交换状态,每个消息里带一个"我负责哪些槽"的 bitmap。16384 bit = 2KB;如果用 65536,bitmap 就是 8KB,Gossip 消息体积翻四倍,节点一多带宽就吃紧。
  • 节点规模上限。一个 Cluster 实际能跑的节点数远小于 16384(作者认为 1000 节点以上不现实),槽位给太多纯属浪费。
  • 槽位压缩。bitmap 还可以用 run-length 编码进一步压缩,16384 这个量级压缩效果好。

3. key 到槽的映射,是对整个 key 做哈希,还是只对一部分? 默认对整个 key。但有个例外——hash tag,下一节专门讲。

text
key="user:1001:profile"
        │
        ▼
   CRC16("user:1001:profile")        ← 16 位 CRC
        │
        ▼
   mod 16384                         ← 落到 0–16383 之间
        │
        ▼
   slot = 12543
        │
        ▼
   查 slot→node 映射表 → 命中 Master C

4. 节点怎么知道哪个槽归谁? 每个节点都维护一份全局的 slot → node 映射(存在 nodes.conf 里,通过 Gossip 在节点间同步)。客户端也维护一份这份映射的缓存,发命令前先本地算槽、查映射,直接打到目标节点,避免每次都走重定向。这也是为什么智能客户端(Lettuce、go-redis、ioredis、redis-py-cluster)比 redis-cli -c 重要——前者缓存了映射、能做客户端路由,后者每次靠服务端重定向。

一个 3 主节点的 Cluster,默认槽位分配几乎均匀:

text
$ redis-cli -p 7000 cluster nodes
<id-A> 127.0.0.1:7000@17000 myself,master - 0 0 1 connected 0-5460
<id-B> 127.0.0.1:7001@17001 master      - 0 0 2 connected 5461-10922
<id-C> 127.0.0.1:7002@17002 master      - 0 0 3 connected 10923-16383

注意每个节点开了两个端口:7000 是客户端端口,@17000(客户端端口 + 10000)是集群总线端口,节点间用二进制协议通过它做 Gossip、故障检测、配置同步。生产上这两个端口都要在安全组/防火墙里放开,否则节点之间探测不到,集群起不来。

三、Hash Tag:让相关 key 住在同一个槽

Cluster 的分片是"按 key 哈希",这带来一个直接后果:你想一次操作多个 key,但这些 key 被哈希到了不同的槽,就操作不了。比如:

text
MSET user:1:name alice  user:2:name bob
(error) CROSSSLOT Keys in request don't hash to the same slot

user:1:name 和 user:2:name 哈希到了不同的槽,Cluster 直接拒绝。这就是"跨槽限制",后面会专门讲。但很多时候,业务上确实有一组 key 在逻辑上是"一伙的",必须能一起操作——一个用户的购物车、一个订单的多个字段、一次事务要改的多个 key。Hash Tag 就是为此而生。

规则很简单:如果 key 里有 {...}(花括号包围的子串),Redis 只对花括号里的内容做哈希。

text
key                 hash 的内容    落点
─────────────────────────────────────────────
user:{1001}:cart    "1001"         slot(CRC16("1001"))
user:{1001}:profile "1001"         slot(CRC16("1001"))   ← 同一槽!
user:{1001}:orders  "1001"         slot(CRC16("1001"))   ← 同一槽!
order:{5002}:items  "5002"         slot(CRC16("5002"))   ← 不同槽

只要花括号里的 tag 一样,这三个 key 就必然落到同一个槽、同一个节点。于是:

text
MSET user:{1001}:name alice  user:{1001}:email [email protected]
OK                              ← 同槽,允许

事务、Lua、MGET 只要 key 用了同一个 tag,都能跨 key 操作。

用 hash tag 的纪律:

  1. 别滥用。 把一堆热点 key 强行塞进同一个 tag,等于把它们强制绑死到一个槽、一个节点——这个节点会成为热点,分片的意义就被破坏了。tag 只用于"业务上必须原子操作的一组 key",比如同一个用户、同一个订单相关的 key。
  2. tag 要够离散。 如果你的 tag 是 {2024}(年份),那 2024 年所有数据挤在一个槽,直接倾斜。tag 要保证不同业务实体分散——用实体 ID({userId}、{orderId})而非类别。
  3. 规则是"第一个 { 到第一个 }"。 user:{1001}:{extra} 只认 1001;foo{bar{baz} 这种嵌套花括号,只取最外层第一对。空花括号 {} 不生效,正常按整串哈希。

一句话:hash tag 是 Cluster 里"我就是要这几个 key 在一起"的逃生舱。用对了,跨槽原子操作轻松搞定;用错了,热点直接烧穿一个节点。

四、主从复制与 Gossip:Cluster 的高可用底座

光分片还不够——每个分片的主节点宕了,那一段槽位就全不可用。所以 Cluster 在每个主节点后面挂从节点(replica),主挂了从顶上。这层和单机的主从复制本质一样,但有几个 Cluster 特有的细节。

每个槽都有一个主、至少一个从。 3 主 3 从是最低可用配置:

text
分片 A:  Master A (槽 0-5460)   ← replication → Replica A
分片 B:  Master B (槽 5461-10922) ← replication → Replica B
分片 C:  Master C (槽 10923-16383)← replication → Replica C

主负责自己槽位的读写(从默认只复制、不对外服务,除非 cluster-allow-reads-when-down 或读请求显式打到从)。复制机制和普通主从一样:全量同步(RDB + 增量缓冲)+ 命令传播,这块不展开。

Cluster 的关键不是复制本身,而是节点之间怎么"知道彼此还活着"——靠 Gossip。

Gossip 协议的工作方式:每个节点每隔一小段时间(cluster-node-timeout 的一部分),随机挑几个节点发 PING,PING 里带:

  • 我的状态(是否存活、是不是 master/replica)
  • 我负责哪些槽(slot bitmap)
  • 我知道的集群里其他节点的状态(gossip section,一批别的节点的 IP/状态摘要)

对方回 PONG,带同样的信息。这样集群状态在节点间持续扩散、收敛。一个节点挂了,几十秒内全集群都会通过 Gossip 知道。

cluster-node-timeout 是最关键的调参项(默认 15 秒):

  • 设太短:网络抖一下就误判节点挂了,频繁触发不必要的 failover。
  • 设太长:节点真挂了要等很久才被发现,故障窗口拉长。
  • 生产经验:内网稳定环境 5-10 秒,跨机房或网络不稳的 15-30 秒。要配合心跳间隔(cluster-ping-interval)一起看,让心跳频率远高于超时时间,否则抖动误判。

几个不要忘的配置:

conf
port 7000                        # 客户端端口
cluster-enabled yes              # 开启集群模式(不开就是普通 Redis)
cluster-config-file nodes-7000.conf  # 集群状态文件,Redis 自己维护,别手改
cluster-node-timeout 15000       # 节点失联多久判 PFAIL,生产调 5-15s
cluster-announce-ip 10.0.0.1     # NAT/容器环境下必须显式声明对外 IP
cluster-announce-port 7000
cluster-announce-bus-port 17000
cluster-require-full-coverage yes # 任意槽无人负责时整个集群拒绝写(默认 yes)

cluster-announce-* 是容器部署的命门。 Docker/K8s 里,Redis 看到的自己是容器内网 IP,但 Gossip 要把这个 IP 广播给别的节点——别的节点拿这个内网 IP 根本连不上。必须用 cluster-announce-ip 把宿主机 IP / 对外可达 IP 声明出去,否则集群在容器里组不起来、或者节点之间互相连不上。

五、自动故障转移:检测、选举、提升

主节点挂了,Cluster 要自动把对应的从提升成新主,接管槽位。这个过程叫 failover,分三步:检测 → 选举 → 提升。

第一步:检测(PFAIL → FAIL)。

  • 节点 A 给节点 B 发 PING,超过 cluster-node-timeout 还没回 PONG,A 把 B 标记为 PFAIL(probable failure,疑似下线)。
  • A 通过 Gossip 把"B 可能挂了"广播出去。
  • 当超过半数的主节点都把 B 标记为 PFAIL,集群就把 B 升级为 FAIL(确定下线)。
  • FAIL 状态再通过 Gossip 扩散到全集群。

为什么要半数主节点确认,而不是一个节点说了算?防误判。一个节点可能因为自己网络故障、看不到 B,但 B 其实好好的。要半数主节点都看不到 B,才能比较确定是真的挂了。

第二步:选举(从节点发起,主节点投票)。

  • B 的从节点(假设叫 B')发现 B 已经 FAIL,启动选举。
  • B' 增加自己的 config epoch(配置纪元,一个单调递增的版本号),向主节点们拉票。
  • 主节点们按"谁先来投谁"的规则,给一个选举纪元里投出一票(一个主节点一票)。
  • B' 拿到过半数主节点的票,就赢得选举。

这里的"过半数主节点投票"机制,和 Raft/Paxos 的 quorum 思路一致——保证同一时间只有一个从节点能当选,避免两个从都自以为是新主(脑裂)。

选举还有两个偏序优化,避免选出一个数据最旧的从:

  • replica-priority(slave-priority):从节点的优先级,数字越小越优先。设为 0 表示永不当选(用于纯数据备份节点)。
  • replication offset:优先级相同时,选复制偏移量最大(数据最新)的从。
  • run id:都一样时,按 run id 字典序最小的。

第三步:提升(B' → 新主)。

  • B' 赢得选举后,把自己升级为 master。
  • 通过 Gossip 广播一个 PONG,带上新的 config epoch 和"我现在负责这些槽"。
  • 其他节点收到后,更新自己的槽映射,把 B 的槽指向 B'。
  • 从此客户端打到这些槽的请求,被路由到 B'。

整个 failover 通常在几十秒内完成(cluster-node-timeout + 一点选举延迟)。这段时间里,落到故障分片槽位的写请求会失败(除非从节点开了读,读还能凑合)。这就是 Cluster 的故障窗口,也是为什么核心数据要开持久化——failover 后新主是基于它已有的复制数据,可能丢掉最后几秒没同步的写。

text
Master B 挂了                          Gossip 半数主确认 → FAIL
      │                                          │
      ▼                                          ▼
  Replica B' 发现 B FAIL → 发起选举 → 主节点投票(quorum)
                                                     │
                                                     ▼
                                    B' 当选 → 升级 master
                                    → 广播 PONG(新 epoch)
                                    → 全集群更新槽映射到 B'

手动 failover。 有时候你想主动切(比如主节点要下线维护),不想等它真挂。从节点执行 CLUSTER FAILOVER:

text
# 在从节点上执行,主动接管
CLUSTER FAILOVER          # 默认 TAKEOVER,先确保数据同步完再切
CLUSTER FAILOVER FORCE    # 强制,不等主确认
CLUSTER FAILOVER TAKEOVER # 手动夺取(不受 quorum 限制,脑裂风险,慎用)

正常运维用默认的 CLUSTER FAILOVER(无参数)就行,它会让从先把主从同步追平、再切,最安全。TAKEOVER 是灾难恢复用的,平时别碰。

六、客户端路由:MOVED 与 ASK

Cluster 客户端的核心职责是:本地算 slot → 查 slot→node 映射 → 直接打到目标节点。但映射不是静态的——节点会加减、槽会迁移、主会切换。客户端打错了节点,Redis 会返回两种重定向,这是 Cluster 协议的灵魂。

MOVED:永久重定向。

客户端把命令发到了 A,但这个槽现在归 C(A 不负责了)。A 返回:

text
-MOVED 12543 127.0.0.1:7002

意思是"槽 12543 永久归 127.0.0.1:7002 管"。客户端应该:

  1. 更新本地的 slot→node 映射(把 12543 指向 7002)。
  2. 重新发命令到 7002。

MOVED 是"映射过时了"的信号,更新映射后,后续对同一个槽的请求就直接命中,不再重定向。智能客户端会在收到 MOVED 时刷新整个映射(或增量更新),保持映射新鲜。

ASK:临时重定向(迁移中)。

槽位正在从 A 迁移到 C 的过程中,这个槽同时存在于两边——A 上还剩一部分没迁走的 key,C 上已经有迁过去的 key。客户端打到 A 查一个 key:

  • 如果 key 还在 A,A 正常处理。
  • 如果 key 已经迁到 C,A 返回 -ASK 12543 127.0.0.1:7002。

ASK 是"这一次请你去 C 问,但映射别更新"。客户端的行为:

  1. 不更新本地映射(因为这只是迁移中的一次性情况,迁移完了这个槽还是归 C,但映射更新机制另说,期间更新会乱)。
  2. 给 C 先发一个 ASKING 命令,再发真正的命令。ASKING 是个"一次性通行证"——告诉 C"我知道这个槽正在导入,请处理我这个请求",没有它 C 会拒绝(因为槽还没正式归 C)。
  3. 只重定向这一次,下次同槽还是先查本地映射(本地映射仍指向 A,直到迁移完成收到 MOVED 才改)。

MOVED vs ASK 的本质区别:

MOVEDASK
语义槽永久归新节点槽正在迁移,这次去新节点
客户端动作更新本地映射 + 重发不更新映射 + ASKING + 重发
触发场景路由过时、failover 后slot 迁移过程中
后续同槽请求直接到新节点仍可能回老节点(直到迁移完)
频率偶发(映射刷新后消失)迁移期间持续
text
客户端发 GET user:{1001}:profile
        │
  本地映射: slot(CRC16("1001")) → A
        │
        ▼
  打到 A ──┬── key 在 A → 正常返回
           │
           └── key 已迁到 C → -ASK 12543 C:7002
                                │
                                ▼
                客户端: ASKING → GET(打到 C,不更新本地映射)

实战要点:

  • 用智能客户端。 让客户端库维护映射、处理 MOVED/ASK,别自己手撸重定向逻辑。go-redis、ioredis、Lettuce、redis-py 都成熟。
  • MOVED 抖动要看。 生产里如果 MOVED 频繁出现,说明客户端映射过时严重——通常是刚做完 failover 或 reshard,客户端缓存没及时刷新。健康集群稳态下 MOVED 应该接近 0。
  • redis-cli -c 只是演示用。 它能自动跟随重定向,但每次都走服务端重定向、不缓存映射,性能差,生产别当客户端用。

七、跨槽限制:MGET/事务/Lua 为什么报错

Cluster 的分片让"多 key 操作"变成了一个需要小心对待的事。一句话原则:任何涉及多个 key 的命令,所有 key 必须落在同一个槽,否则报 CROSSSLOT 错误。

受影响的操作:

1. 多 key 命令。 MGET、MSET、DEL key1 key2、EXISTS k1 k2——所有 key 同槽才行:

text
MGET user:{1001}:name user:{1001}:email      ← 同 tag 同槽,OK
MGET user:1001 user:1002                      ← 不同槽,CROSSSLOT

2. 事务(MULTI/EXEC)。 事务里所有 key 必须同槽:

text
MULTI
HSET user:{1001}:profile name alice
SADD user:{1001}:tags vip
EXEC                              ← key 同 tag,OK

跨槽的话,MULTI 阶段就报错。

3. Lua 脚本。 脚本里访问的所有 key,必须在脚本执行前通过 KEYS[] 显式声明,且全部同槽:

text
EVAL "return redis.call('GET', KEYS[1])" 1 user:{1001}:name
                                   ↑
                              必须用 KEYS[] 声明,不能在脚本里拼 key

为什么?因为 Cluster 要在执行前判断"这个脚本涉及哪些 key、它们是不是同槽",如果不通过 KEYS[] 声明,Cluster 没法做这个校验(脚本内容它不解析)。所以Cluster 下 Lua 脚本里严禁动态拼接 key 名——所有要访问的 key 必须是参数传入的、且用同一 hash tag。

4. 集合运算。 SUNION、SINTER、SDIFF、ZUNIONSTORE 等多 key 集合操作,同样要求同槽。

绕过办法只有一个:hash tag。 把业务上要一起操作的 key 用 {tag} 绑到同一槽。这是 Cluster 设计里刻意留的口子——它知道有些 key 在业务上是一伙的,给你一个显式声明"它们要在一起"的机制。

一个真实的设计取舍: 假设你有一个用户系统,每个用户有 profile、cart、orders 几组 key。你想做"查某用户的所有信息"的多 key 操作。

  • 用 {userId} 做 tag:user:{1001}:profile、user:{1001}:cart——这用户的 key 都在一个槽,能 MGET/事务。代价:这用户的所有数据绑死在一个节点,这个用户超活跃会成为热点。
  • 不用 tag,散开分布:多 key 操作做不了,要发多次单 key 请求。代价:失去原子性和批量效率。

选哪个看业务。绝大多数场景,按实体 ID 做 tag 是对的——单用户再活跃,单实体的数据量也不会大到撑爆一个槽;而失去原子操作会逼你把逻辑搬到应用层,复杂度更高。

八、Cluster vs Sentinel:到底该选哪个

这是被问得最多的问题。先把两者定位划清楚:

SentinelCluster
数据分布单数据集,所有 key 在一个主节点分片,key 分散到多个主节点
高可用主从 + 哨兵监控 + 自动 failover每分片主从 + 自动 failover
水平扩展无(写吞吐=单核,内存=单机)有(写吞吐和内存随分片线性扩展)
客户端普通 Redis 客户端(哨兵只做选主)必须支持 Cluster 协议的客户端
多 key 操作无限制(都在一个节点)跨槽受限(需 hash tag)
运维复杂度低高(迁移、Gossip、跨节点)
适用规模单机内存够用、写 QPS < 10 万数据 > 单机舒适区、写吞吐需扩展

一句话定位:Sentinel = 单数据集的高可用;Cluster = 分片 + 高可用。

Sentinel 解决的问题是"我的 Redis 不能是单点"。它在主从基础上加几个 Sentinel 进程(奇数个,通常 3 或 5),监控主节点健康,主挂了自动把从提升成新主,并通知客户端切到新主。数据没有分片——所有 key 还在一个主节点上,写吞吐受限于单核,内存受限于单机。

Cluster 解决的问题是"单机装不下/单核写不动"。它在 Sentinel 的高可用之上,加了分片。每个分片内部就是一套主从 + failover,跨分片做数据分散。

选型决策树:

text
你的 Redis 数据集 + 增长预期,能放进单机舒适内存吗(一般 ≤ 30-50GB)?
│
├── 能 → 写 QPS 是否顶到单核上限(~10-15 万)?
│        │
│        ├── 没有 → Sentinel(或主从)。别上 Cluster,徒增复杂度。
│        └── 是的 → Cluster(要扩写吞吐,只能分片)。
│
└── 不能 → Cluster。数据量逼着你分片,没得选。

我的实战观点:

  • 绝大多数中小规模业务,Sentinel 就够了。 一个主从 + 3 个 Sentinel,覆盖会话、缓存、限流、排行榜,数据量在十几个 GB 以内,写 QPS 几万——这覆盖了 90% 的场景。Sentinel 客户端简单、多 key 操作没限制、运维心智负担小。
  • 只有数据量真的大、或者写吞吐真的打满单核,才上 Cluster。 不要为了"显得高级"而上 Cluster,它带来的客户端复杂度、跨槽限制、迁移运维,会让团队持续交税。
  • 混合策略也常见: 按业务拆 Redis 实例。缓存用一个 Sentinel 集群,会话用另一个,计数器用又一个——每个都独立、规模可控、互不影响。比把所有东西塞进一个大 Cluster 要好维护。

上不上 Cluster,看的是"单机装不装得下、单核写不写得动",不是"看起来够不够分布式"。Sentinel 是高可用,Cluster 是分片+高可用——你不需要分片的时候,Cluster 就是给自己找麻烦。

九、扩缩容实战:slot 迁移与脑裂处理

Cluster 真正考验运维的是扩缩容和脑裂。这两块没踩过的人,上 Cluster 就是在赌。

扩容:加节点 + 迁槽

加一个新主节点 D,要把它"塞进"集群并分配一些槽给它:

text
# 1. 启动一个 Redis 实例,cluster-enabled yes
# 2. 把它加入集群
redis-cli --cluster add-node 127.0.0.1:7003 127.0.0.1:7000

# 3. 从现有节点迁移一部分槽到 D(交互式,会问你迁多少、给谁)
redis-cli --cluster reshard 127.0.0.1:7000
# How many slots? -> 4096
# Receiving node id? -> <D 的 node id>
# Source node ids? -> all(或指定来源)

# 4. 给 D 配从节点(复制 D)
redis-cli --cluster add-node 127.0.0.1:7004 127.0.0.1:7000 \
          --cluster-slave --cluster-master-id <D 的 node id>

reshard 底层做的事,是把槽迁移拆成单 key 的搬运。一个槽的迁移过程:

text
# 在源节点 A 上:把槽 1234 标记为"正在迁出"到 D
CLUSTER SETSLOT 1234 MIGRATING <D-node-id>

# 在目标节点 D 上:把槽 1234 标记为"正在从 A 导入"
CLUSTER SETSLOT 1234 IMPORTING <A-node-id>

# 搬 key(逐个或批量)
MIGRATE <D-ip> <D-port> "" 0 5000 KEYS key1 key2 ...

# 搬完后,在所有节点上把槽 1234 指向 D
CLUSTER SETSLOT 1234 NODE <D-node-id>

迁移期间这个槽是"双写状态"——A 上还剩没搬的 key,D 上有已搬的 key。这正是 ASK 重定向出现的场景:客户端打到 A 查一个已搬走的 key,A 返回 ASK 让它去 D。迁移期间业务无感,只是部分请求会多一跳 ASK。

实战注意:

  • reshard 是在线操作,但别在高峰期跑。 大 key(列表、hash、zset 几万 field)的 MIGRATE 是阻塞的,源节点和目标节点都要卡顿。先 redis-cli --bigkeys 摸清大 key,错峰迁。
  • 用 --cluster-yes 跳过确认只在脚本里。 手动操作一定要看清楚 reshard 的交互提示。
  • 迁完用 --cluster check 验证槽分布均匀。

缩容:删节点 + 收回槽

缩容是扩容的反向:先把 D 的槽迁回别的节点,再把 D 从集群踢掉。

text
# 1. 把 D 的槽迁到别的节点(reshard,源=D,目标=其他)
redis-cli --cluster reshard 127.0.0.1:7000
# How many slots? -> <D 当前拥有的槽数>
# Receiving node? -> <某个保留节点的 id>
# Source? -> <D 的 node id>

# 2. 删节点(先删从,再删主)
redis-cli --cluster del-node 127.0.0.1:7000 <D-from-node-id>
redis-cli --cluster del-node 127.0.0.1:7000 <D-node-id>

最容易出的错:没把 D 的槽迁空就直接删。 这样那些槽就没人负责了,集群 cluster-require-full-coverage yes 的话直接整体拒绝写。删主节点前,必须确认它的槽数为 0。

脑裂:两个主都认为自己负责同一个槽

脑裂是 Cluster 最危险的故障。典型场景:网络分区让集群一分为二。

  • 分区 1:A(主,槽 0-5460)+ 多数主节点
  • 分区 2:A 的从 A' + 少数主节点

如果分区 2 错误地触发 A' 当选(虽然它拿不到多数主节点的票,正常不该当选),就会出现 A 和 A' 都认为自己是槽 0-5460 的主——两个主都接受写,数据分叉。

Redis Cluster 靠几个机制防脑裂:

1. 选举的 quorum。 从节点要拿到过半数主节点的票才能当选。分区 2 里只有少数主节点,A' 拿不到多数票,不该当选。这是最核心的防线。

2. config epoch 单调递增 + 全局唯一。 每次成功的 failover 都会让新主的 config epoch 比所有现存主都高。Gossip 扩散时,epoch 高的覆盖低的。即使短暂出现两个主,网络恢复后 epoch 低的那个会被踢掉,槽归 epoch 高的。

3. min-replicas-to-write / min-replicas-max-lag。 这是给主节点设的"我得看到至少 N 个从、且延迟在 X 秒内才接受写"。分区里孤立的主节点看不到从,直接拒绝写,避免它继续接受写导致分叉。

conf
min-replicas-to-write 1     # 至少看到 1 个从才接受写(默认 0,建议设 1)
min-replicas-max-lag 10     # 从的延迟不能超过 10 秒

实战建议:

  • min-replicas-to-write 1 是生产标配。 哪怕只有一个从,也能挡住"主节点被孤立后继续接受写"的脑裂。代价是如果从挂了主会短暂拒绝写,但这比脑裂后数据分叉好处理得多。
  • 奇数个主节点。 3、5、7 个主节点,保证 quorum 能形成。偶数个主节点(4、6)在网络对半分时两边都拿不到多数,集群整体不可写。
  • cluster-require-full-coverage yes(默认)。 任何一个槽无人负责,整个集群拒绝写。宁可全停也不接受"部分数据"的写——避免后续合并的痛苦。
  • 脑裂已经发生了怎么办? 别试图自动合并。停掉一边、人工核对数据、用 CLUSTER RESET 重置错的节点再重新加入。脑裂恢复是脏活,预防永远比治疗容易。

十、运维检查清单

上线前过一遍,能避免大半的 Cluster 事故:

  1. 节点数:至少 3 主 3 从。 3 主保证 quorum,从保证单分片 failover。
  2. cluster-node-timeout 调到 5-15 秒。 默认 15 秒偏慢,内网调 5-10 秒故障窗口更短。
  3. cluster-announce-* 在容器/NAT 环境下必须配。 否则 Gossip 广播的是内网 IP,节点互连不上。
  4. 客户端总线端口(客户端端口 + 10000)要在防火墙放开。 Gossip 走它,不通集群起不来。
  5. min-replicas-to-write 1。 防脑裂的最小代价防线。
  6. 客户端用支持 Cluster 的库。 别用 redis-cli -c 当生产客户端,别用不支持 Cluster 的老库。
  7. 大 key 迁移前先摸排。 redis-cli --bigkeys,reshard 错峰,避免迁移卡顿业务。
  8. 持久化别忘开。 每个分片的主节点都要开 AOF(everysec + 混合模式)。Cluster 只管高可用,不管数据持久化——failover 后新主只有它复制到的数据,没复制到的丢就丢了。
  9. CLUSTER NODES / CLUSTER INFO 定期巡检。 看 cluster_state 是不是 ok,看槽分布是否均匀,有没有节点长期 fail。
  10. 演练 failover。 故意杀一个主,看从是否在预期时间内顶上。没演练过的 HA,等于没有 HA。

小结

  1. Cluster 的本质是"分片 + 高可用":16384 个槽按 CRC16 分配到多个主节点,每个主节点带从做自动 failover。上 Cluster 的唯一硬指标是"单机装不下"或"单核写不动",其他场景 Sentinel 就够。
  2. slot = CRC16(key) % 16384 是一切的基础;hash tag {tag} 是"让相关 key 住同一槽"的逃生舱,用对了解锁跨 key 原子操作,用错了制造热点。
  3. MOVED 是永久重定向(更新映射),ASK 是迁移中的临时重定向(ASKING + 不更新映射)——智能客户端替你处理这两者,自己别手撸。
  4. 跨槽是 Cluster 的硬限制:MGET、事务、Lua、集合运算的多 key 必须同槽,否则 CROSSSLOT。解法只有一个:hash tag。
  5. Cluster 不等于 Sentinel:Sentinel 是单数据集的高可用,Cluster 是分片 + 高可用。选 Sentinel 还是 Cluster,看的是数据量和写吞吐,不是"够不够分布式"。
  6. 脑裂靠 quorum + config epoch + min-replicas-to-write 三道防线;奇数主节点、开最小从写入保护,是预防脑裂的标配。

分片不是银弹,它只是把"单机装不下"这个问题,换成了"多机怎么协调、怎么迁移、怎么防脑裂"这一整套新问题。Cluster 的价值不在"能用",而在于你清楚知道它每一次 MOVED、ASK、failover 背后发生了什么——以及它什么时候会失败。


延伸阅读:

Related

相关推荐