在实际业务里,Redis 单机撑不住的时候,绝大多数团队的第一反应就是上 Redis Cluster。但很多人对 Cluster 的使用只停留在“redis-cli --cluster create 一把梭”的层面,一旦遇到槽位迁移、CROSSSLOT 报错、CLUSTERDOWN,就完全不知道怎么排查。这篇文章我想把 Redis Cluster 的数据分片机制完整拆一遍:从为什么分片、怎么分片、客户端怎么路由,到实际搭建和扩缩容的完整流程,以及我踩过的坑。适合想把集群彻底搞明白的运维、后端开发和架构师,也适合准备面试的人做系统性梳理。
1. 为什么需要数据分片:单机Redis的边界
1.1 单机Redis的瓶颈到底卡在哪
很多人以为 Redis 快,所以不存在性能问题。单机 Redis 确实快,但它有非常明显的三个天花板:容量、吞吐、可用性。
容量是最先撞上的墙。假设你要存 5 亿个用户维度的 key,每个 key 加上关联数据平均按 1KB 算,那就是 500GB 内存。单机不可能扛得住,而且 Redis 的内存不是全部能用来装业务数据的——AOF 重写、RDB 快照、内存碎片、备份预留都要占空间,实际可用容量一般只有物理内存的 60% 左右。一台 64GB 的机器,能放心用的业务缓存也就 40 多 GB。
吞吐方面,Redis 的命令执行是单线程事件循环,虽然极快,但总归有上限。纯 KV 操作短连接压测能到十万级 QPS,一旦命令复杂(SINTERSTORE、ZUNIONSTORE、大 value 的 GETSET),吞吐立刻往下掉。更要命的是慢查询会堵住后续所有请求,单机上你几乎没有容错空间。
可用性方面,单实例挂了就是全挂,只能靠主从加哨兵保命。但哨兵模式解决的是“高可用”,解决不了“数据总量和吞吐上限”的问题。你内存就那么大,加再多的从节点,从节点的存储总量也是天花板,写能力也没有本质提升。
所以当数据量和流量都到一定程度时,必须做分片:把数据拆成多份,分散到多台机器上,让整个集群的容量和吞吐量可以随节点数量水平扩展。这就是 Redis Cluster 存在的根本原因。
1.2 三种分片方案对比:客户端分片、代理分片、官方Cluster
先聊一个常见误区:分片不是 Redis Cluster 发明的。在 Cluster 出现之前,业界已经有各种分片方案,各有各的取舍。理解这些方案,你才能明白 Cluster 的设计到底好在哪。
第一种是客户端分片。代表方案是 Jedis 的 ShardedJedis。客户端内置一致性哈希,根据 key 直接算出该连哪台 Redis。优点是没有中间层,性能最高,实现也简单;缺点是分片逻辑焊死在客户端代码里,换语言就要重写一套,而且扩缩容时存量数据的迁移完全要自己搞定,一不留神就会把线上数据搞乱。很多团队用了一阵就被迁移成本劝退了。
第二种是代理分片。代表方案是 Twemproxy 和 Codis。客户端无脑连代理,代理负责路由和转发,后面挂多少 Redis 实例客户端完全无感。好处是客户端接入简单,与语言无关;缺点是中间多一跳,延迟和带宽都有损耗,代理本身还需要额外的高可用保障,而且代理对命令的支持打了折扣,很多复杂命令直接不支持。
第三种就是官方的 Redis Cluster。它用固定数量的哈希槽来分片,数据和槽绑定,槽再分配到各个主节点。客户端需要实现重定向逻辑,服务端通过 gossip 协议维护集群状态,支持在线扩缩容,主节点故障后从节点自动提升。它把客户端分片和代理分片的优势整合了,代价是功能上牺牲了一部分——跨 slot 的多 key 操作变得极其受限,后面我会专门讲。
三种方案我列个表,方便直观对比:
| 方案 | 路由规则在哪 | 扩缩容 | 多key操作 | 性能开销 | 代表性实现 |
|---|---|---|---|---|---|
| 客户端分片 | 客户端 | 需自研迁移 | 仅同分片可用 | 最低 | ShardedJedis、一致性哈希 |
| 代理分片 | 代理层 | 代理层规划迁移 | 仅同分片可用 | 中间一跳损耗 | Twemproxy、Codis |
| Redis Cluster | 客户端+服务端配合 | 在线reshard | 仅同slot可用 | 低,靠客户端路由 | 官方Cluster |
Redis Cluster 的核心聪明之处在于:它把“数据分片”这件事抽象成了“哈希槽分配”问题。数据和槽的映射是固定算法,槽和节点的映射是动态可变的。这样数据分布与节点数量解耦,扩容时只需要搬动部分槽位,而不是重新哈希全部数据。
2. 哈希槽:Redis Cluster分片的核心设计
2.1 16384个槽位如何分配到节点
Redis Cluster 的数据分片不是直接把 key 哈希到节点,而是先哈希到一个逻辑概念——槽(slot)。整个集群固定有 16384 个槽,编号从 0 到 16383。每个主节点负责其中一段槽位区间,集群启动时通过cluster addslots命令或者redis-cli --cluster create自动分配。
以一个 3 主节点的集群为例,槽位分配是这样的:
- 节点 A:0 - 5460
- 节点 B:5461 - 10922
- 节点 C:10923 - 16383
每个 key 经过哈希计算落在其中一个槽上,这个槽归哪个节点管,读写请求就去哪个节点。所以槽是数据迁移和负载均衡的最小单位。数据迁移不需要按 key 一条条商量,而是以槽为单位整体搬移,粒度比一致性哈希的“虚拟节点”更可控,也更均匀。
如果你自己手动配置,可以用cluster addslots 0 1 2 ...把指定槽号分配给当前节点。手动分配很麻烦,所以生产上基本都用redis-cli --cluster create自动分配,它会把 16384 个槽尽量平均地分给每个主节点。
2.2 key如何映射到槽位:CRC16与取模
key 到槽的映射是固定算法,保证任何客户端、任何时刻计算出来的结果都一致。公式很简单:
slot = CRC16(key) % 16384CRC16 是一种循环冗余校验算法,它能把任意长度的字符串计算成一个 16 位的整数。Redis 使用的是 CRC16-CCITT 的一个变体,% 16384相当于是取这个 16 位整数的低 14 位(因为 2 的 14 次方正好是 16384)。源码里它做了查表优化,性能极高,一次计算也就几十纳秒。
我用 Python 演示一下计算原理,方便你本地验证:
import crcmod crc16 = crcmod.predefined.Crc('xmodem') def hash_slot(key: str) -> int: crc16.new() crc16.update(key.encode()) return crc16.crcValue % 16384 print(hash_slot("foo")) # 12182这里用xmodem多项式近似模拟 Redis 的 CRC16 算法,原理一致。foo这个 key 会落到第 12182 号槽。至于这个槽在哪个节点,取决于槽和节点的映射。
这里必须提一个使用频率极高的特性:hash tag。Redis Cluster 支持在 key 里用花括号{}指定哈希标签,计算槽位时只用花括号内的部分参与哈希。举个例子:
{user:1000}.following{user:1000}.followers
这两个 key 都只对user:1000做 CRC16,所以它们必然落在同一个 slot 上。这个特性是解决跨 slot 多 key 操作问题的唯一官方钥匙,后面讲批量操作时还会反复提到。
2.3 为什么是16384而不是65536
很多人第一次看到 16384 这个数字都会问:为什么不用 65536?这样槽位数更多,数据分布更均匀,迁移粒度也更细。Redis 作者 antirez 在 GitHub 上专门回应过这个问题,核心原因有几个。
第一是心跳消息的带宽成本。集群节点之间要频繁交换状态信息,每个节点需要告诉其他节点“我负责哪些槽”。实现方式是一个槽位位图(bitmap),每个槽占 1 个 bit。16384 个槽就是 16384 bit,换算下来是 2KB;如果换成 65536 个槽,位图就是 8KB。节点数量越多,心跳消息在网络上的放大效应越明显,每秒广播一次,8KB 的成本翻 4 倍,对带宽是实打实的压力。
第二是集群规模上限。16384 个主节点的规模理论上已经够大,而实际上一个集群跑到 1000 个主节点就已经非常夸张,再往上光网络和故障恢复的复杂度就足以让人崩溃。既然实际规模不可能到 65536 个主节点,就没必要用 65536 个槽。
第三是数据迁移的粒度权衡。槽数越多,每个槽包含的数据量越小,迁移越平滑;但槽数过多会让元数据膨胀、心跳包变大、槽位分配变得碎片化。16384 是个很均衡的选择:常见千节点集群下,每个节点平均十几个槽,迁移粒度足够细,元数据开销也控得住。
2.4 槽位信息如何同步:gossip协议与配置纪元
槽位的分配信息不是只存在某个中心节点上,而是每个节点都保存一份完整的集群视图。Redis Cluster 用 gossip 协议(闲聊协议)来同步这些信息,节点之间通过 PING/PONG 消息交换各自的槽位 bitmap 和节点状态。
gossip 的机制很有意思:每个节点每隔大约 100ms 随机挑一个节点发送 PING,收到后返回 PONG,消息里携带自己的槽位信息、节点状态和配置纪元。这样一个集群的状态会在秒级内收敛到所有节点。它不需要中心化协调器,也不存在“元数据服务器挂了集群就挂”的问题。
这里有个关键概念叫配置纪元(configEpoch)。它本质上是一个版本号,用来仲裁槽位冲突。比如某个节点发生故障转移,新的主节点会把自己的 configEpoch 加一,然后通过 gossip 广播。当其他节点发现同一个槽位被两个节点认领时,谁的 configEpoch 大就听谁的。这套机制保证集群在分区、故障、重连等复杂情况下,最终能达成一致的槽位归属。
3. 客户端如何找到数据:重定向与路由机制
3.1 MOVED重定向:槽位归属变了
当客户端向节点 A 请求一个 key,但 A 发现自己不负责这个 key 对应的槽时,就会返回一个 MOVED 错误。比如连接 7000 端口的节点,执行GET foo,foo的槽是 12182,而 12182 归 7001 端口的节点管,响应会是这样的:
(error) MOVED 12182 127.0.0.1:7001这个响应告诉你两件事:第一,槽 12182 已经永久归属于 127.0.0.1:7001;第二,你本地缓存的槽位映射该更新了。Smart 客户端收到 MOVED 后,会更新本地缓存,然后重新向目标节点发送命令。如果你用的是命令行工具redis-cli,不加-c参数就只能看到这行错误,加了-c才会自动跟随重定向。
MOVED 是“永久”重定向。也就是说客户端下次查这个槽的 key,应该直接找到新节点,不应该再问旧节点。所以收到 MOVED 后务必更新本地映射表,否则每次请求都白跳一次,性能损耗非常明显。
3.2 ASK重定向:迁移过程中的临时引导
与 MOVED 容易混淆的是 ASK 重定向。它出现在槽位迁移过程中。假设槽 12182 正在从节点 A 迁往节点 B,但迁移还没完成,槽位的归属在集群状态里仍然算 A。此时请求访问 A 的某个 key,如果这个 key 已经迁走了,A 会返回:
(error) ASK 12182 127.0.0.1:7001ASK 和 MOVED 的本质区别是:ASK 是“临时的”,槽的归属并没有变,只是这一次有个 key 已经搬到目标节点了,你去那边找一下;MOVED 是“永久的”,槽的归属彻底变了。客户端的处理也不同:收到 ASK 后,不能更新本地缓存里的槽位映射,只是临时向目标节点发起一次请求。
而且,收到 ASK 后客户端不能直接发 GET,而是要先发送一个ASKING命令,再发真正的请求。为什么?因为正常来说,目标节点 B 并不负责这个槽,如果直接发 GET,B 也会返回 MOVED 把你踢回去。ASKING 命令的作用是告诉 B:“我知道这个槽还不归你管,但这是迁移过程中的临时请求,请破例帮我处理这一次。”这样请求才能成功。
用表格对比一下就非常清晰:
| 类型 | 触发时机 | 槽位归属 | 客户端是否更新缓存 | 是否需要ASKING |
|---|---|---|---|---|
| MOVED | 槽位永久迁移完成 | 已变更 | 是 | 不需要 |
| ASK | 槽位正在迁移中 | 尚未变更 | 否 | 需要 |
3.3 Smart客户端的槽位缓存与失效更新
理解了 MOVED 和 ASK,再看客户端路由就很清楚了。JedisCluster、Lettuce 这类 Smart 客户端在初始化时,会随机连一个节点执行CLUSTER SLOTS命令,拿到全量槽位分布,然后在本地维护一张“槽号 -> 节点”的映射表。
之后每次请求先查本地映射表,直接连对应节点,不需要每一条命令都问一遍集群元数据,所以性能很高。当集群发生扩缩容、故障转移、reshard 时,客户端本地映射可能会短暂过期。这时它依赖两件事来恢复:一是 MOVED 错误触发映射更新,二是客户端自身的重试机制。很多客户端收到 MOVED 后重试一次就能成功,但如果连续遇到集群状态变化,可能重试多次,所以客户端超时时间要设置得合理。
这里有个细节值得注意:Smart 客户端的映射表更新是“按槽”的,不是按 key 的。某个槽被迁走后,客户端后续对这个槽的所有 key 都会走新节点。这也意味着,如果你手动对某个槽做了迁移,要留意旧节点的连接池里可能还有残留连接,不过 Redis 协议层面的重定向机制能兜住,不会产生数据正确性问题。
3.4 分片带来的功能限制
分片的代价是实打实的:一切涉及多个 key 的操作,如果这些 key 不在同一个 slot 上,就无法执行。最常见的是MGET、MSET、DEL多 key、RENAME、事务和 Lua 脚本。
比如执行MGET user:1 user:2,如果这两个 key 的槽位不同,节点会直接返回错误:
(error) CROSSSLOT Keys in request don't hash to the same slotLua 脚本也一样,Redis Cluster 会检查脚本里用到的所有 key 是否在同一个 slot,不是同一个就拒绝执行。事务MULTI/EXEC虽然没有显式检查所有 key?实际上在 Cluster 模式下也要求所有 key 在同一 slot,否则要么报错,要么行为不符合预期。
应对思路有三种。第一种最推荐:设计 key 时就考虑分片,把业务上需要一起操作的 key 用 hash tag 固定在同一个 slot,比如user:{1000}:profile、user:{1000}:orders,这样单个用户的数据天然聚集;第二种是客户端分组:把 key 按 slot 分组,每个组单独 pipeline 请求,实现跨节点的聚合逻辑;第三种是尽量避免跨 key 操作,能分步拆就拆。
我在实际项目里吃过亏:早期没做 key 规范,上线后想用事务批量更新用户多个维度的信息,结果被 CROSSSLOT 卡住,最后只能改 key 结构。所以建议在项目开始前就把 key 的命名规范定好,hash tag 该加就加,真等数据量起来之后再改,成本高到难以想象。
4. 实操:搭建集群与在线扩缩容
4.1 最小可用集群的搭建流程
先说环境。生产环境建议至少三台物理机,每台上跑一个主节点和一个从节点,形成 3 主 3 从的标准结构。测试环境可以在一台机器上起多个实例模拟,但要注意端口、数据目录、日志文件全部隔离。
每个实例的配置最少要加这几项:
# redis-7000.conf port 7000 cluster-enabled yes cluster-config-file nodes-7000.conf cluster-node-timeout 15000 cluster-require-full-coverage yes appendonly yes daemonize yes pidfile /var/run/redis-7000.pid logfile /var/log/redis-7000.log解释几个关键项:
| 配置项 | 作用 | 建议 |
|---|---|---|
| cluster-enabled yes | 开启集群模式 | 必须 |
| cluster-config-file | 集群状态持久化文件,每次启动都会读写 | 每个实例独立文件名 |
| cluster-node-timeout | 节点心跳超时时间,超时判定主节点故障 | 默认15000ms,网络抖动的环境适当调大 |
| cluster-require-full-coverage | 槽位不全时是否停止服务 | 默认yes,要求高可用可考虑no,但需评估风险 |
| appendonly yes | 开启AOF,保证实例重启后数据不丢 | 生产必须开 |
所有实例启动后,用一条命令完成集群创建:
redis-cli --cluster create \ 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \ 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \ --cluster-replicas 1--cluster-replicas 1表示每个主节点配一个从节点。命令执行时会有一次交互,展示槽位分配计划和主从配对关系,确认无误后输入yes。之后可以验证集群状态:
redis-cli --cluster check 127.0.0.1:7000输出里会显示每个节点的 ID、负责的槽位范围、从节点是谁、是否有槽位未分配。检查信息正常后,用-c模式验证读写:
redis-cli -c -p 7000 127.0.0.1:7000> set foo bar -> Redirected to slot [12182] located at 127.0.0.1:7001 OK看到Redirected说明路由机制已经生效。如果不加-c,这里就会返回 MOVED 错误。
4.2 在线扩容:新增节点与reshard
集群跑起来之后,最常用的运维操作就是扩容。比如现在要加一个 7006 端口的新节点,先启动实例,然后加入集群:
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000注意新节点加入后没有任何槽位,也不能服务数据。你必须手动执行 reshard 把一部分槽迁给它。执行 reshard 同样是一条命令:
redis-cli --cluster reshard 127.0.0.1:7000它会进入交互模式,问三件事:要迁移多少个槽、接收槽的节点 ID 是哪个、从哪些源节点迁出。假设我迁移 1000 个槽给 7006 节点,源节点选择all,就是平均从现有三个主节点各抽一部分槽,新节点就获得了分布均匀的 1000 个槽。整个过程是逐槽进行的,迁移期间集群对外服务不中断,这是 Redis Cluster 对比客户端分片的巨大优势。
缩容反过来操作:先执行 reshard 把目标节点的所有槽迁移给其他节点,然后用del-node把它踢出集群:
redis-cli --cluster del-node 127.0.0.1:7000 <node-id>一个我踩过的坑:删节点前必须确认目标节点的槽位已经归零。如果还有槽就del-node,操作会被拒绝;如果强行走流程,集群状态会进入异常区,必须马上重新分配槽位恢复。
4.3 数据迁移的核心命令MIGRATE
reshard 看起来像黑盒魔法,其实底层就是一条命令在反复执行:MIGRATE。MIGRATE 的工作流程是这样的:源节点把 key 序列化导出(包含值和过期时间),通过网络发送给目标节点,目标节点接收后写入自己的数据集,返回成功,源节点再删除本地 key。
命令长这样:
MIGRATE 127.0.0.1 7006 key 0 5000 REPLACE其中5000是超时毫秒数,REPLACE表示目标节点如果已有同名的 key 就直接覆盖。由于 MIGRATE 是单 key 操作,数据量大时一个个迁移会很慢,所以redis-cli --cluster reshard内部会做流水线优化,一次连接里连续迁移多个 key,降低 RTT 的影响。手动调优时可以关注--cluster-pipeline参数,它控制每个连接内连续的迁移命令数量,适当调大能显著提升 reshard 速度。
需要提醒的是:迁移过程中如果源节点和目标节点之间网络抖动,可能发生 key 同时在两边都存在的情况。MIGRATE 的底层实现已经做了比较完善的容错,但为了安全,迁移完一轮后建议用redis-cli --cluster check检查一遍槽位覆盖,确认没有 key 落在错误节点上。
5. 常见问题与避坑指南
5.1 热点key与哈希槽倾斜
Redis Cluster 只解决数据总量问题,解决不了“所有访问都打在一个 key 上”的热点问题。由于 hash 算法的确定性,同一个 key 永远落在同一个槽、同一个节点,热点 key 的访问压力也全部集中在那一个节点上。
表现就是某台节点 CPU 飙升、网络打满,其他节点闲得发慌。排查时用redis-cli --hotkeys扫高频访问的 key,再用CLUSTER INFO看各节点内存和请求分布,基本能定位。
处理热点 key 有几种常用手法:
- 给 key 加随机后缀分散到多个槽,比如热点
news:top拆成news:top:1、news:top:2等,业务侧读的时候随机选一个副本。这是最立竿见影的手段,代价是该 key 的批量操作会受限。 - 加一层本地缓存,把热点数据缓存在应用进程内,扛住绝大多数读取。
- 把热点 key 的从节点数增多,并通过客户端配置从节点读,分摊读压力。
要特别警惕 hash tag 的滥用:千万不要为了让多个业务 key 落在同一槽就把所有 key 都套同一个 tag。这样会导致大量不相干的数据挤在同一个槽上,不仅触发节点内存倾斜,还会让热点被无限放大。hash tag 的使用边界就是“确实需要在一起执行多 key 操作的 key”。
5.2 批量操作与CROSSSLOT错误
线上最常见的问题就是CROSSSLOT。报错信息非常直白:一次请求里的 key 没有哈希到同一个槽。错误提示是:
(error) CROSSSLOT Keys in request don't hash to the same slot这个错误在MGET、MSET、DEL多 key、RENAME、事务和 Lua 脚本中都可能出现。我观察到的典型场景是:运营后台有个聚合查询脚本,一次拉取几十个用户的昵称和等级,用MGET一把梭,上了集群直接报错。
解决思路优先级排列如下:
- 改 key 设计,用 hash tag 把强相关的 key 固定到同一槽,这是最省事的方案;
- 客户端按 slot 分组聚合请求,比如把几十个用户按 slot 分几组,每组发一次 pipeline,再把结果合并,不用改数据;
- 实在无法避免跨槽事务,只能业务补偿或引入独立的协调服务,复杂度和成本都很高。
我踩坑后的原则是:凡是业务上明确需要一起读写的 key,必须共享同一个业务 ID,并在命名上强制包含{业务ID}标签。这个规范在集群环境里是命脉,越早建立越好。
5.3 CLUSTERDOWN与集群可用性
集群模式一个反直觉的地方:默认配置下,只要有一个槽位没有主人,整个集群就会拒绝所有请求,返回CLUSTERDOWN The cluster is down。触发条件包括:某个主节点宕机且没有从节点可以顶上、手动迁移过程中槽位出现了短暂无人持有的间隙、节点网络分区导致槽位视图不一致。
这个行为由cluster-require-full-coverage yes控制。它的设计逻辑是极端保守的:既然数据不完整,宁可整体不可用,也不能让业务拿到不完整的数据。对于金融、交易类场景这是合理的;但对于缓存类场景,很多团队会选择把它改成no,这样部分槽挂了,其他槽还能正常读写,代价是你得接受数据缺失的现实。
排查 CLUSTERDOWN 的标准流程:
redis-cli -p 7000 cluster info # cluster_state:fail # cluster_slots_assigned:16384 # cluster_slots_pfail:100 redis-cli -p 7000 cluster nodes重点看cluster_state、cluster_slots_pfail和cluster_slots_fail。如果发现有主节点处于fail状态,且它的从节点没有及时提升,先看cluster-node-timeout是否设置过大,再看从节点的数据复制是否落后太多。还有一个小坑:测试环境用FLUSHALL清库后,如果重启集群节点时忘了删cluster-config-file,可能会出现旧节点 ID 不匹配导致的槽位丢失,这种问题排查起来最耗时间。
5.4 故障转移与脑裂边界
主节点挂了之后,从节点会在cluster-node-timeout之后发起故障转移投票。投票由集群里其他存活的 master 节点参与,得票超过半数才能成为新主。这也是为什么 Redis Cluster 最少要 3 个主节点:只有 2 个主时,一个挂了,另一个只有 1 票,达不到多数,无法完成转移,集群就断了。
这里必须说清楚一个本质:Redis Cluster 是 AP 系统,不是 CP 系统。在极端网络分区情况下,旧主节点可能并没有真正宕机,只是和集群其他节点失联了。如果旧主仍然接受了客户端的写入,而这些写入没有来得及复制给从节点,那么新主通过故障转移上位后,旧主那些“孤岛写入”就会永久丢失。这就是脑裂丢数据的核心风险。
所以涉及资金、订单这类强一致性的数据,不能只靠 Redis Cluster 自身机制兜底。常见做法是:给关键数据的写操作加一层应用侧校验,或者直接用数据库作为强一致存储,Redis Cluster 只承担缓存角色。理解了这一点,你再看各种“集群数据丢失”的爆料,本质上都不是 Redis 的 bug,而是对 CAP 边界预期错位。
6. 最后的实操心得
整套数据分片机制啃下来,我最大的体会是:Redis Cluster 的技术难点不在搭建和命令,而在对分片规则的敬畏。数据会落在哪个节点,不是靠运气,是 CRC16 算出来的确定性结果;哪些 key 能一起操作,是由哈希槽决定的硬约束。所有问题,根源都在设计阶段有没有围绕这些约束把 key 结构设计好。
另外,运维层面一定要留好后路:生产环境做 reshard 前,先在一套临时集群演练一遍;迁移完成后立即执行redis-cli --cluster check检查完整性;CLUSTERDOWN 不是玄学,大多数时候是槽位缺失或者配置纪元冲突,用cluster nodes的输出基本能锁定肇事节点。这些习惯看起来简单,但真到线上出问题时,能帮你省下大把的抢救时间。