【Redis从入门到精通】第50篇:集群重新分片——不停服迁移槽位的黑魔法
2026/6/4 0:37:55 网站建设 项目流程

上一篇【第49篇】MOVED和ASK——Cluster重定向机制详解
下一篇【第51篇】Cluster复制与故障转移——节点挂了怎么办(明日更新,敬请期待)


好了,集群搭起来了,数据也分布好了。但好景不长——用户量激增,内存告急,老板说加两台机器。加机器容易,但数据怎么搬过去?停服迁数据?老板说不可能,7×24不能断!

这就是重新分片(Resharding)要解决的问题:在不停止服务的情况下,把槽从一个节点迁移到另一个节点。这不是简单的MOVE命令就能搞定的——槽里有成千上万个key,每个都在被持续读写。今天我们就来拆解这个"黑魔法"。

一、重新分片的使用场景

┌──────────────────────────────────────────────────┐ │ 重新分片的常见场景 │ ├──────────────────────────────────────────────────┤ │ │ │ 场景1:扩容 │ │ 3台机器 → 5台机器 │ │ 从每个旧节点移一些槽到新节点 │ │ │ │ 场景2:缩容 │ │ 5台机器 → 3台机器 │ │ 把要下掉的节点的槽迁移到保留的节点 │ │ │ │ 场景3:槽分布不均 │ │ 某节点槽太多/太大,内存使用率98% │ │ 迁移部分槽到负载低的节点 │ │ │ │ 场景4:流量倾斜 │ │ 热门key集中某几个槽,对应节点CPU打满 │ │ 重新分配这些槽到多个节点分散压力 │ │ │ └──────────────────────────────────────────────────┘

无论哪种场景,核心操作都一样:把N个槽从源节点迁移到目标节点。Redis Cluster的重新分片之所以是"黑魔法",是因为它做到了:

  1. 不中断服务:迁移过程中对key的读写请求不被拒绝
  2. 数据不丢失:迁移过程原子化,要么全成功要么失败回滚
  3. 客户端无感知:通过ASK重定向透明处理
  4. 可观测:通过命令实时监控迁移进度

二、迁移一个槽的完整流程

先看一个槽从头到尾是怎么搬走的。下图是单槽迁移的完整生命周期:

┌─ 源节点 (Node A) ───────┐ ┌─ 目标节点 (Node B) ───────┐ │ │ │ │ │ slots[slot] = Node A │ │ slots[slot] != Node B │ └──────────────────────────┘ └───────────────────────────┘ │ │ │ ① redis-cli向Node B发送 │ │ CLUSTER SETSLOT <slot> IMPORTING <A> │ → Node B记录: 这个槽的key正在从A迁进来 │─────────────────────────────────────────→ │ │ │ ② redis-cli向Node A发送 │ │ CLUSTER SETSLOT <slot> MIGRATING <B> │ → Node A记录: 这个槽的key正在迁到B │←────────────────────────────────────────│ │ │ │ ③ 获取槽中的所有key │ │ CLUSTER GETKEYSINSLOT <slot> <count> │←────────────────────────────────────────│ │ │ │ ④ 逐个迁移key │ │ MIGRATE <B_ip> <B_port> <key> 0 <timeout> │ → Node A: DUMP key → 序列化数据 │ → 发送序列化数据到Node B │ → Node B: RESTORE key │ → Node B返回OK │ → Node A: DEL key │─────────────────────────────────────────→ │ │ │ ⑤ 重复③④直到槽中key全迁移 │ │ │ │ ⑥ redis-cli向双方发送 │ │ CLUSTER SETSLOT <slot> NODE <B_id> │ → 正式宣布: slot归Node B了! │─────────────────────────────────────────→ │ │ │ ⑦ Gossip协议传播新槽分配 │ │ 全集群节点更新slots数组 │ │ │ 迁移完成! ┌──────────────────┐ ┌───────────────────┐ │ slots[slot]=NULL │ │ slots[slot]=Node B │ └──────────────────┘ └───────────────────┘

这个流程中每一步都是可恢复的:

  • 如果在①②之后失败 →CLUSTER SETSLOT <slot> STABLE回退
  • 如果在④中途失败 → key在源节点还在,重试MIGRATE
  • 如果在⑥之前失败 →CLUSTER SETSLOT <slot> NODE <A_id>回退

只有第⑥步执行后,槽才算真正换了主人。

三、MIGRATE命令:一石三鸟的原子操作

MIGRATE是迁移的核心武器,它内部做了三件事:

MIGRATE <target_ip> <target_port> <key> <destination-db> <timeout> 内部执行流程: ┌─────────────────────────────────┐ │ 1. DUMP key │ │ 将key序列化为二进制数据 │ │ 格式: 类型码 + CRC64 + 数据 │ ├─────────────────────────────────┤ │ 2. 发送序列化数据到目标节点 │ │ 通过TCP连接发送 │ │ 目标节点执行 RESTORE │ │ 带 REPLACE 参数可覆盖已有key │ ├─────────────────────────────────┤ │ 3. 在源节点上 DEL key │ │ 只有RESTORE成功才执行 │ │ 删除释放源节点内存 │ └─────────────────────────────────┘

这是个伪原子操作——它不是传统数据库的事务原子性,但通过"先DUMP再RESTORE最后DEL"的顺序,确保了数据不会重复也不会丢失。

具体保证:

  • DUMP成功但RESTORE失败 → key还在源节点,数据没丢
  • RESTORE成功但DEL失败 → key在两个节点各有一份(重复但不丢)——需要通过REPLACE标记处理
  • RESTORE成功且DEL成功 → 完美迁移
# 迁移单个keyMIGRATE192.168.1.1026380"user:1001"05000# MIGRATE参数说明:# 192.168.1.102 6380 → 目标节点# "user:1001" → 要迁移的key# 0 → 目标数据库编号(Redis Cluster始终为0)# 5000 → 超时时间(毫秒)# 批量迁移(一次请求迁移多个key)MIGRATE192.168.1.1026380""05000KEYS key1 key2 key3# 带COPY选项:迁移但保留源key(测试用)MIGRATE192.168.1.1026380"user:1001"05000COPY# 带REPLACE选项:目标存在则覆盖MIGRATE192.168.1.1026380"user:1001"05000REPLACE

MIGRATE的底层实现

// MIGRATE的内部实现(src/cluster.c 简化版)voidmigrateCommand(client*c){// 1. 判断是阻塞模式还是非阻塞模式// timeout=0 → 非阻塞(返回错误给客户端)// timeout>0 → 阻塞(当前连接等待结果)// 2. 创建到目标节点的Socket连接intfd=anetTcpConnect(...);// 3. DUMP keyrioInitWithBuffer(&payload,...);rdbSaveObject(&payload,o);// 序列化// 4. 发送RESTORE命令到目标节点// RESTORE key <ttl> <serialized_value> [REPLACE]// TTL = PTTL(key) 用于保留过期时间// 5. 读取目标节点的回复// 成功 → DEL key on source// 失败 → 保留key}

踩坑提示:MIGRATE是阻塞命令。在迁移大key时(比如一个100MB的String或100万元素的Set),MIGRATE会阻塞源节点的事件循环。虽然有MIGRATE内部异步I/O,但DUMP序列化和数据复制都在主线程执行。迁移大key前,一定要评估key的大小——超过10MB的key建议用业务迁移方案,别用MIGRATE硬搬。

四、redis-cli --cluster reshard 实操

手动一步步执行MIGRATE太麻烦了。redis-cli --cluster reshard是官方提供的自动化重分片工具:

redis-cli--clusterreshard127.0.0.1:6379

交互式流程:

>>> Performing Cluster Check (using node 127.0.0.1:6379) M: ... 127.0.0.1:6379 slots:[0-5460] (5461 slots) master M: ... 127.0.0.1:6380 slots:[5461-10922] (5462 slots) master M: ... 127.0.0.1:6381 slots:[10923-16383] (5461 slots) master [OK] All nodes agree about slots configuration. How many slots do you want to move (from 1 to 16384)?

输入要迁移的槽数:

How many slots do you want to move (from 1 to 16384)? 1000 What is the receiving node ID?

输入目标节点ID(从上面的NODES输出中复制):

What is the receiving node ID? a1b2c3d4e5... # 新节点ID Please enter all the source node IDs. Type 'all' to use all the nodes as source nodes for the hash slots. Type 'done' once you entered all the source nodes IDs. Source node #1:

这里可以指定从哪些节点迁出。输入all表示从所有现有主节点平均提取:

Source node #1: all Ready to move 1000 slots. Source nodes: M: ... 127.0.0.1:6379 slots:[0-5460] M: ... 127.0.0.1:6380 slots:[5461-10922] M: ... 127.0.0.1:6381 slots:[10923-16383] Destination node: M: ... 127.0.0.1:6384 slots: (0 slots) Resharding plan: Moving slot 0 from 127.0.0.1:6379 to 127.0.0.1:6384 Moving slot 1 from 127.0.0.1:6379 to 127.0.0.1:6384 Moving slot 2 from 127.0.0.1:6379 to 127.0.0.1:6384 ... Do you want to proceed with the proposed reshard plan (yes/no)?

确认后开始迁移:

Do you want to proceed with the proposed reshard plan (yes/no)? yes Moving slot 0 from 127.0.0.1:6379 to 127.0.0.1:6384: ... Moving slot 1 from 127.0.0.1:6379 to 127.0.0.1:6384: . Moving slot 2 from 127.0.0.1:6379 to 127.0.0.1:6384: . ...

每个.代表一个槽迁移成功。工具内部自动执行前面说的完整迁移流程。

非交互模式

# 自动分配(一行命令搞定)redis-cli--clusterreshard127.0.0.1:6379\--cluster-from<source-node-id>\--cluster-to<target-node-id>\--cluster-slots1000\--cluster-yes# 从所有节点平均移出redis-cli--clusterreshard127.0.0.1:6379\--cluster-from all\--cluster-to<target-node-id>\--cluster-slots1000\--cluster-yes

检查迁移后的槽分布

redis-cli--clustercheck127.0.0.1:6379# 查看新节点上的槽redis-cli-p6384CLUSTER NODES|grepmyself

五、在线迁移过程中访问正在迁移的key

在第49篇中我们详细讨论了MOVED和ASK的区别。在reshard过程中,ASK机制确保数据持续可访问:

迁移中访问key的处理逻辑: ┌─ 源节点 (MIGRATING状态) ─┐ ┌─ 目标节点 (IMPORTING状态) ─┐ │ │ │ │ │ 收到 GET key │ │ 收到 GET key │ │ → key还在本地? │ │ → 有ASKING标记? │ │ 有 → 正常返回 │ │ 有 → 本地查找并返回 │ │ 无 → 返回ASK跳转 │ │ 无 → 返回MOVED给源节点 │ │ │ │ │ │ 注意:MIGRATING状态下 │ │ 注意:IMPORTING状态下 │ │ 90%的请求仍走本地, │ │ 只有被ASKING许可的请求 │ │ 只有已迁移的key才ASK │ │ 才会处理 │ └────────────────────────────┘ └────────────────────────────┘

时间线视角:

Key: "user:1001" 所在槽 slot=8416 时间点1: 槽正常归属于Node A GET user:1001 → Node A返回value ✓ 时间点2: CLUSTER SETSLOT 8416 IMPORTING/MIGRATING → 开始迁移 GET user:1001 → Node A还有这份数据 → 正常返回 ✓ 时间点3: MIGRATE user:1001 到 Node B(key已迁走) GET user:1001 → Node A发现key不在 → 返回 ASK → 客户端转到Node B → 发ASKING → GET user:1001 → Node B返回value ✓ 时间点4: CLUSTER SETSLOT 8416 NODE B(迁移完成) GET user:1001 → Node A返回 MOVED → 客户端更新缓存 → 转到Node B → Node B直接返回value ✓

整个过程中,只有MIGRATE执行的那一瞬间key不可用(毫秒级),其他时间都有明确的路由。

六、大Key迁移与性能影响

在reshard过程中,大Key(big key)是最让人头疼的问题。

大Key的定义与影响

┌───────────────────────────────────────────────┐ │ 大Key对迁移的影响 │ ├───────────────────────────────────────────────┤ │ │ │ String类型:> 10MB │ │ → DUMP序列化时瞬间分配大量内存 │ │ → 网络传输时间长,可能超时 │ │ │ │ 集合类型:> 10000个元素 │ │ → 序列化和反序列化耗时随元素数线性增长 │ │ → DEL操作释放内存可能触发操作系统的内存回收 │ │ │ │ 总体影响: │ │ → MIGRATE阻塞源节点主线程 │ │ → 迁移过程中该key不可访问 │ │ → 引发客户端超时 │ │ │ └───────────────────────────────────────────────┘

应对策略

# 1. 迁移前扫描大Keyredis-cli--bigkeys-p6379# 2. 用MEMORY USAGE估算key大小redis-cli-p6379MEMORY USAGE"big_hash_key"# (integer) 10485760 # 10MB!# 3. 评估该槽有多少大keyredis-cli-p6379CLUSTER COUNTKEYSINSLOT8416# (integer) 512# 4. 分批迁移,避开大key的槽# 如果slot 8416有超大key,可以:# - 先迁移其他小key的槽# - 对大key所在槽,业务低峰期单独迁移# - 或者把大key拆分成小key后再迁移

踩坑提示:MIGRATE的超时时间计算不是简单的"传输时间"。默认timeout参数是5000ms,但对大key来说远远不够。如果1秒还没迁移完,Redis内部会进行重试。每个重试周期都会重新DUMP key——如果key有10MB,每秒重试一次就是每秒重新分配10MB内存。内存的频繁申请和释放会把源节点拖垮。

MIGRATE内部的重试机制

MIGRATE的timeout不是总超时时间,而是单次传输的超时。如果key太大导致单次传输失败,Redis会自动重试:

// MIGRATE的内部超时处理(简化)while(remaining_time>0){// 发起连接和传输if(sendRestoreCommand(fd,key,payload)==C_OK){// 成功了!delKeyOnSource(key);return;}// 超时了但还有时间,重试remaining_time-=elapsed;}// 所有时间用完,返回错误

这就解释了为什么大key迁移可能卡住整个reshard流程——MIGRATE在一个大key上反复重试,阻塞后续槽的迁移。

业务侧迁移大key的方案

对于超大key(>50MB),建议不用MIGRATE,改为业务迁移:

业务侧大key迁移方案: 1. 向目标节点写入新key(不同key名) 如: big_key → big_key_new 2. 双写过渡期(源+目标都写) 或者先全量复制再增量追 3. 验证数据一致性 4. 切换读取到目标节点 5. 删除源节点旧key,迁移完成

七、迁移过程的监控

迁移是个漫长过程(可能需要几个小时),必须有完善的监控。

实时监控命令

# 查看集群状态(关注slots_ok是否为16384)redis-cli-p6379CLUSTER INFO|grep-E"state|slots_ok|slots_pfail"# 查看迁移状态的节点redis-cli-p6379CLUSTER NODES|grep-E"migrating|importing"# 输出示例:# a1b2c3... 192.168.1.101:6379@16379 myself,master - 0 ... 1 connected 0-5460 [3999->-a1b2c3...] [8416->-d4e5f6...]# [3999->-a1b2c3...] 表示 slot 3999 正在迁出到 a1b2c3# [3999-<-a1b2c3...] 表示 slot 3999 正在从 a1b2c3 迁入

迁移进度估算

# 估算剩余迁移时间redis-cli-p6379--clustercheck127.0.0.1:6379# 输出会显示:# [OK] All 16384 slots covered.# Slot 0-5460 covered by node a1b2c3... (5461 slots)# ...# 对比迁移前后的槽分布变化

一个简单的监控脚本

#!/bin/bash# monitor_reshard.sh - 监控重分片进度HOST="127.0.0.1"PORT="6379"INTERVAL=10# 每10秒检查一次echo"= Redis Cluster Reshard Monitor ="echo"Time | State | Slots_OK | Migrating | Importing"echo"--------------------|-------|----------|-----------|----------"whiletrue;doINFO=$(redis-cli-h$HOST-p$PORT CLUSTER INFO2>/dev/null)NODES=$(redis-cli-h$HOST-p$PORT CLUSTER NODES2>/dev/null)STATE=$(echo"$INFO"|grep"cluster_state"|cut-d:-f2|tr-d'\r')SLOTS_OK=$(echo"$INFO"|grep"cluster_slots_ok"|cut-d:-f2|tr-d'\r')# 统计迁移中的槽数MIGRATING=$(echo"$NODES"|grep-o"\->-"|wc-l)IMPORTING=$(echo"$NODES"|grep-o"<-"|wc-l)TIME=$(date"+%Y-%m-%d %H:%M:%S")printf"%-20s| %-6s| %-9s| %-10s| %-10s\n"\"$TIME""$STATE""$SLOTS_OK""$MIGRATING""$IMPORTING"sleep$INTERVALdone

性能影响监控

迁移期间务必关注这些指标:

监控指标正常阈值关注阈值报警阈值说明
网络带宽<500Mbps>800Mbps>900Mbps迁移会消耗源和目标的带宽
源节点内存稳定持续下降-迁移后DEL释放内存,应该下降
目标节点内存稳定持续上升>80%接收数据导致内存增长
源节点CPU<50%>70%>85%DUMP序列化消耗CPU
目标节点CPU<50%>70%>85%RESTORE反序列化消耗CPU
源节点延迟<1ms>3ms>5msMIGRATE阻塞可能影响其他请求
重定向率<1%>3%>10%MOVED/ASK比例异常升高
客户端超时0偶发持续大key迁移导致的超时

总结

集群重新分片是Redis Cluster运维中的最核心技能。从CLUSTER SETSLOT设置迁移状态,到MIGRATE的DUMP+RESTORE+DEL原子操作,再到redis-cli --cluster reshard的自动化编排——整条链路设计精妙,确保数据在迁移过程中持续可访问。

关键要点:

  1. IMPORTING/MIGRATING状态是迁移的"安全网",让客户端通过ASK找到正确数据
  2. MIGRATE是原子操作,数据不丢不重
  3. 大key是迁移的噩梦,尽量拆分
  4. 监控是迁移的"眼睛",带宽、CPU、内存、延迟一个不能少

迁移完成后,别忘了还需要为新节点配置从节点,确保高可用。下一篇,我们看Cluster内部如何实现自动故障转移——没有Sentinel,节点挂了怎么自动恢复?


上一篇【第49篇】MOVED和ASK——Cluster重定向机制详解
下一篇【第51篇】Cluster复制与故障转移——节点挂了怎么办(明日更新,敬请期待)


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询