上一篇【第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的重新分片之所以是"黑魔法",是因为它做到了:
二、迁移一个槽的完整流程
先看一个槽从头到尾是怎么搬走的。下图是单槽迁移的完整生命周期:
┌─ 源节点 (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成功 → 完美迁移
# 迁移单个key
MIGRATE 192.168.1.102 6380 "user:1001" 0 5000
# MIGRATE参数说明:
# 192.168.1.102 6380 → 目标节点
# "user:1001" → 要迁移的key
# 0 → 目标数据库编号(Redis Cluster始终为0)
# 5000 → 超时时间(毫秒)
# 批量迁移(一次请求迁移多个key)
MIGRATE 192.168.1.102 6380 "" 0 5000 KEYS key1 key2 key3
# 带COPY选项:迁移但保留源key(测试用)
MIGRATE 192.168.1.102 6380 "user:1001" 0 5000 COPY
# 带REPLACE选项:目标存在则覆盖
MIGRATE 192.168.1.102 6380 "user:1001" 0 5000 REPLACE
MIGRATE的底层实现
// MIGRATE的内部实现(src/cluster.c 简化版)
void migrateCommand(client *c) {
// 1. 判断是阻塞模式还是非阻塞模式
// timeout=0 → 非阻塞(返回错误给客户端)
// timeout>0 → 阻塞(当前连接等待结果)
// 2. 创建到目标节点的Socket连接
int fd = anetTcpConnect(...);
// 3. DUMP key
rioInitWithBuffer(&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 –cluster reshard 127.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 –cluster reshard 127.0.0.1:6379 \\
–cluster-from <source-node-id> \\
–cluster-to <target-node-id> \\
–cluster-slots 1000 \\
–cluster-yes
# 从所有节点平均移出
redis-cli –cluster reshard 127.0.0.1:6379 \\
–cluster-from all \\
–cluster-to <target-node-id> \\
–cluster-slots 1000 \\
–cluster-yes
检查迁移后的槽分布
redis-cli –cluster check 127.0.0.1:6379
# 查看新节点上的槽
redis-cli -p 6384 CLUSTER NODES | grep myself
五、在线迁移过程中访问正在迁移的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. 迁移前扫描大Key
redis-cli –bigkeys -p 6379
# 2. 用MEMORY USAGE估算key大小
redis-cli -p 6379 MEMORY USAGE "big_hash_key"
# (integer) 10485760 # 10MB!
# 3. 评估该槽有多少大key
redis-cli -p 6379 CLUSTER COUNTKEYSINSLOT 8416
# (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 -p 6379 CLUSTER INFO | grep -E "state|slots_ok|slots_pfail"
# 查看迁移状态的节点
redis-cli -p 6379 CLUSTER 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 -p 6379 –cluster check 127.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 "——————–|——-|———-|———–|———-"
while true; do
INFO=$(redis-cli -h $HOST -p $PORT CLUSTER INFO 2>/dev/null)
NODES=$(redis-cli -h $HOST -p $PORT CLUSTER NODES 2>/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 $INTERVAL
done
性能影响监控
迁移期间务必关注这些指标:
| 网络带宽 | <500Mbps | >800Mbps | >900Mbps | 迁移会消耗源和目标的带宽 |
| 源节点内存 | 稳定 | 持续下降 | – | 迁移后DEL释放内存,应该下降 |
| 目标节点内存 | 稳定 | 持续上升 | >80% | 接收数据导致内存增长 |
| 源节点CPU | <50% | >70% | >85% | DUMP序列化消耗CPU |
| 目标节点CPU | <50% | >70% | >85% | RESTORE反序列化消耗CPU |
| 源节点延迟 | <1ms | >3ms | >5ms | MIGRATE阻塞可能影响其他请求 |
| 重定向率 | <1% | >3% | >10% | MOVED/ASK比例异常升高 |
| 客户端超时 | 0 | 偶发 | 持续 | 大key迁移导致的超时 |
总结
集群重新分片是Redis Cluster运维中的最核心技能。从CLUSTER SETSLOT设置迁移状态,到MIGRATE的DUMP+RESTORE+DEL原子操作,再到redis-cli –cluster reshard的自动化编排——整条链路设计精妙,确保数据在迁移过程中持续可访问。
关键要点:
迁移完成后,别忘了还需要为新节点配置从节点,确保高可用。下一篇,我们看Cluster内部如何实现自动故障转移——没有Sentinel,节点挂了怎么自动恢复?
上一篇【第49篇】MOVED和ASK——Cluster重定向机制详解 下一篇【第51篇】Cluster复制与故障转移——集群高可用机制




