Redis Cluster:分片、高可用与故障转移
单 Redis 扛不住内存/吞吐时,Redis Cluster 水平扩展。本文讲清 Cluster 的分片模型(16384 槽位 + hash tag)、主从复制与自动故障转移、客户端路由(MOVED/ASK)、跨槽操作限制,以及什么时候该上 Cluster(>单机内存/高吞吐)什么时候用哨兵就够。
本文要点
- 主题聚焦:DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
我把 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 的主因——主因永远是"单机装不下了"或"单核写不动了"。
单 Redis 实例(含主从 + 哨兵)
┌─────────────────────────┐
客户端 → │ Master (全量数据) │ ← 单核写 QPS 上限 ~10-15 万
│ 内存上限 ≈ 单机物理内存 │ ← fork 延迟、COW 翻倍
└─────────────────────────┘
│ replication
┌────┴────┐
▼ ▼
Replica Replica ← 只分担读,写压不住
Redis Cluster 的解法是:把 key 空间切成 16384 个槽(slot),分散到多个主节点,每个主节点再带从节点做高可用。写吞吐随分片数线性扩展,内存随节点数累加,单分片故障不影响其他分片。
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,下一节专门讲。
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,默认槽位分配几乎均匀:
$ 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 被哈希到了不同的槽,就操作不了。比如:
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 只对花括号里的内容做哈希。
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 就必然落到同一个槽、同一个节点。于是:
MSET user:{1001}:name alice user:{1001}:email [email protected]
OK ← 同槽,允许
事务、Lua、MGET 只要 key 用了同一个 tag,都能跨 key 操作。
用 hash tag 的纪律:
- 别滥用。 把一堆热点 key 强行塞进同一个 tag,等于把它们强制绑死到一个槽、一个节点——这个节点会成为热点,分片的意义就被破坏了。tag 只用于"业务上必须原子操作的一组 key",比如同一个用户、同一个订单相关的 key。
- tag 要够离散。 如果你的 tag 是
{2024}(年份),那 2024 年所有数据挤在一个槽,直接倾斜。tag 要保证不同业务实体分散——用实体 ID({userId}、{orderId})而非类别。 - 规则是"第一个
{到第一个}"。user:{1001}:{extra}只认1001;foo{bar{baz}这种嵌套花括号,只取最外层第一对。空花括号{}不生效,正常按整串哈希。
一句话:hash tag 是 Cluster 里"我就是要这几个 key 在一起"的逃生舱。用对了,跨槽原子操作轻松搞定;用错了,热点直接烧穿一个节点。
四、主从复制与 Gossip:Cluster 的高可用底座
光分片还不够——每个分片的主节点宕了,那一段槽位就全不可用。所以 Cluster 在每个主节点后面挂从节点(replica),主挂了从顶上。这层和单机的主从复制本质一样,但有几个 Cluster 特有的细节。
每个槽都有一个主、至少一个从。 3 主 3 从是最低可用配置:
分片 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)一起看,让心跳频率远高于超时时间,否则抖动误判。
几个不要忘的配置:
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 后新主是基于它已有的复制数据,可能丢掉最后几秒没同步的写。
Master B 挂了 Gossip 半数主确认 → FAIL
│ │
▼ ▼
Replica B' 发现 B FAIL → 发起选举 → 主节点投票(quorum)
│
▼
B' 当选 → 升级 master
→ 广播 PONG(新 epoch)
→ 全集群更新槽映射到 B'
手动 failover。 有时候你想主动切(比如主节点要下线维护),不想等它真挂。从节点执行 CLUSTER FAILOVER:
# 在从节点上执行,主动接管
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 返回:
-MOVED 12543 127.0.0.1:7002
意思是"槽 12543 永久归 127.0.0.1:7002 管"。客户端应该:
- 更新本地的 slot→node 映射(把 12543 指向 7002)。
- 重新发命令到 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 问,但映射别更新"。客户端的行为:
- 不更新本地映射(因为这只是迁移中的一次性情况,迁移完了这个槽还是归 C,但映射更新机制另说,期间更新会乱)。
- 给 C 先发一个
ASKING命令,再发真正的命令。ASKING是个"一次性通行证"——告诉 C"我知道这个槽正在导入,请处理我这个请求",没有它 C 会拒绝(因为槽还没正式归 C)。 - 只重定向这一次,下次同槽还是先查本地映射(本地映射仍指向 A,直到迁移完成收到 MOVED 才改)。
MOVED vs ASK 的本质区别:
| MOVED | ASK | |
|---|---|---|
| 语义 | 槽永久归新节点 | 槽正在迁移,这次去新节点 |
| 客户端动作 | 更新本地映射 + 重发 | 不更新映射 + ASKING + 重发 |
| 触发场景 | 路由过时、failover 后 | slot 迁移过程中 |
| 后续同槽请求 | 直接到新节点 | 仍可能回老节点(直到迁移完) |
| 频率 | 偶发(映射刷新后消失) | 迁移期间持续 |
客户端发 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 同槽才行:
MGET user:{1001}:name user:{1001}:email ← 同 tag 同槽,OK
MGET user:1001 user:1002 ← 不同槽,CROSSSLOT
2. 事务(MULTI/EXEC)。 事务里所有 key 必须同槽:
MULTI
HSET user:{1001}:profile name alice
SADD user:{1001}:tags vip
EXEC ← key 同 tag,OK
跨槽的话,MULTI 阶段就报错。
3. Lua 脚本。 脚本里访问的所有 key,必须在脚本执行前通过 KEYS[] 显式声明,且全部同槽:
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:到底该选哪个
这是被问得最多的问题。先把两者定位划清楚:
| Sentinel | Cluster | |
|---|---|---|
| 数据分布 | 单数据集,所有 key 在一个主节点 | 分片,key 分散到多个主节点 |
| 高可用 | 主从 + 哨兵监控 + 自动 failover | 每分片主从 + 自动 failover |
| 水平扩展 | 无(写吞吐=单核,内存=单机) | 有(写吞吐和内存随分片线性扩展) |
| 客户端 | 普通 Redis 客户端(哨兵只做选主) | 必须支持 Cluster 协议的客户端 |
| 多 key 操作 | 无限制(都在一个节点) | 跨槽受限(需 hash tag) |
| 运维复杂度 | 低 | 高(迁移、Gossip、跨节点) |
| 适用规模 | 单机内存够用、写 QPS < 10 万 | 数据 > 单机舒适区、写吞吐需扩展 |
一句话定位:Sentinel = 单数据集的高可用;Cluster = 分片 + 高可用。
Sentinel 解决的问题是"我的 Redis 不能是单点"。它在主从基础上加几个 Sentinel 进程(奇数个,通常 3 或 5),监控主节点健康,主挂了自动把从提升成新主,并通知客户端切到新主。数据没有分片——所有 key 还在一个主节点上,写吞吐受限于单核,内存受限于单机。
Cluster 解决的问题是"单机装不下/单核写不动"。它在 Sentinel 的高可用之上,加了分片。每个分片内部就是一套主从 + failover,跨分片做数据分散。
选型决策树:
你的 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,要把它"塞进"集群并分配一些槽给它:
# 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 的搬运。一个槽的迁移过程:
# 在源节点 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 从集群踢掉。
# 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 秒内才接受写"。分区里孤立的主节点看不到从,直接拒绝写,避免它继续接受写导致分叉。
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 事故:
- 节点数:至少 3 主 3 从。 3 主保证 quorum,从保证单分片 failover。
cluster-node-timeout调到 5-15 秒。 默认 15 秒偏慢,内网调 5-10 秒故障窗口更短。cluster-announce-*在容器/NAT 环境下必须配。 否则 Gossip 广播的是内网 IP,节点互连不上。- 客户端总线端口(客户端端口 + 10000)要在防火墙放开。 Gossip 走它,不通集群起不来。
min-replicas-to-write 1。 防脑裂的最小代价防线。- 客户端用支持 Cluster 的库。 别用
redis-cli -c当生产客户端,别用不支持 Cluster 的老库。 - 大 key 迁移前先摸排。
redis-cli --bigkeys,reshard 错峰,避免迁移卡顿业务。 - 持久化别忘开。 每个分片的主节点都要开 AOF(everysec + 混合模式)。Cluster 只管高可用,不管数据持久化——failover 后新主只有它复制到的数据,没复制到的丢就丢了。
CLUSTER NODES/CLUSTER INFO定期巡检。 看cluster_state是不是ok,看槽分布是否均匀,有没有节点长期fail。- 演练 failover。 故意杀一个主,看从是否在预期时间内顶上。没演练过的 HA,等于没有 HA。
小结
- Cluster 的本质是"分片 + 高可用":16384 个槽按 CRC16 分配到多个主节点,每个主节点带从做自动 failover。上 Cluster 的唯一硬指标是"单机装不下"或"单核写不动",其他场景 Sentinel 就够。
slot = CRC16(key) % 16384是一切的基础;hash tag{tag}是"让相关 key 住同一槽"的逃生舱,用对了解锁跨 key 原子操作,用错了制造热点。- MOVED 是永久重定向(更新映射),ASK 是迁移中的临时重定向(ASKING + 不更新映射)——智能客户端替你处理这两者,自己别手撸。
- 跨槽是 Cluster 的硬限制:MGET、事务、Lua、集合运算的多 key 必须同槽,否则
CROSSSLOT。解法只有一个:hash tag。 - Cluster 不等于 Sentinel:Sentinel 是单数据集的高可用,Cluster 是分片 + 高可用。选 Sentinel 还是 Cluster,看的是数据量和写吞吐,不是"够不够分布式"。
- 脑裂靠 quorum + config epoch +
min-replicas-to-write三道防线;奇数主节点、开最小从写入保护,是预防脑裂的标配。
分片不是银弹,它只是把"单机装不下"这个问题,换成了"多机怎么协调、怎么迁移、怎么防脑裂"这一整套新问题。Cluster 的价值不在"能用",而在于你清楚知道它每一次 MOVED、ASK、failover 背后发生了什么——以及它什么时候会失败。
延伸阅读:
- Redis 持久化:RDB vs AOF 与混合模式 —— Cluster 只管高可用,数据持久化还得靠每个分片自己开 AOF。
- Redis 在分布式系统的角色 —— 分布式锁、限流、Stream、排行榜,这些原语在 Cluster 下的注意事项。
Related
相关推荐
文章
Docker 部署 Next.js 社区项目的注意事项
围绕 DevOps、Next.js 的技术趋势、工程实践和独立开发视角。
文章
OpenTelemetry 如何帮助你观察 AI 应用?
围绕 DevOps、AI 的技术趋势、工程实践和独立开发视角。
文章
长运行工具调用:Operation、Poll 与 Cancel 的终态协议
初始请求超时后,工具可能仍在服务端运行;发出取消也不等于副作用已停止。本文为单次长运行工具调用建立 operation 身份、轮询水位、取消竞态和唯一终态接受点。
文章
Agent 记忆删除传播:从用户遗忘请求到可验证清除
删除主表一行不会清掉向量、摘要、缓存和恢复后的备份。本文用删除状态机、代际墓碑、派生血缘与端到端 canary 验证真正遗忘。