在这里插入图片描述

👋 大家好,欢迎来到我的技术博客!
💻 作为一名热爱 Java 与软件开发的程序员,我始终相信:清晰的逻辑 + 持续的积累 = 稳健的成长
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕Redis这个话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!


Redis100篇 - Redis集群缩容怕丢数据 节点下线+数据迁移全流程 🛑💾

在构建高可用、高性能的分布式系统时,我们常常谈论“扩容”——如何增加节点来应对流量洪峰和数据增长。但鲜有人深入探讨一个同样重要却更令人紧张的操作:缩容(Scale-in)

当业务进入平稳期,或资源利用率长期偏低时,为了节省成本、优化资源分配,我们不得不面对一个问题:如何安全地从 Redis Cluster 中移除一个或多个节点?

这可不是简单的“关机”或“删除容器”这么简单。如果操作不当,轻则导致部分数据不可访问,重则造成数据永久丢失,引发线上事故。因此,许多运维和开发人员对 Redis 集群缩容心存畏惧,“怕丢数据”成了普遍的心理障碍。

本文将彻底揭开 Redis 集群缩容的神秘面纱,提供一份 完整、安全、可落地的数据迁移与节点下线全流程指南。我们将结合真实命令、Java 代码示例、mermaid 流程图和权威外链,带你一步步完成一次“零数据丢失”的优雅缩容。让你从此不再“怕丢数据”,从容应对资源调整。🛡️


⚠️ 为什么缩容如此“危险”?常见误区与风险

在开始操作之前,我们必须认清缩容的风险来源,避免落入常见误区。

❌ 误区一:直接关闭节点 = 安全下线

最危险的操作莫过于直接 kill 一个 Redis 进程或停止 Docker 容器。这会导致:

  • 如果是主节点,其负责的哈希槽将无人管理,cluster_state 变为 fail,整个集群不可用。
  • 如果是该主节点的从节点也同时宕机或网络隔离,数据将永久丢失。
  • 即使有从节点能晋升为主,但这个过程需要时间,期间服务中断。

❌ 误区二:认为“从节点有数据,主节点删了也无所谓”

虽然从节点会复制主节点的数据,但只有主节点才能对外提供写服务。一旦主节点被移除,即使从节点有数据,客户端也无法写入,集群仍然处于“部分不可用”状态。

❌ 误区三:迁移数据时未处理客户端连接

在迁移过程中,如果客户端没有正确处理 MOVEDASK 重定向,可能会持续向旧节点发送请求,导致数据不一致或请求失败。


✅ 缩容核心原则:先迁移,再下线

安全缩容的唯一正确路径是:

1. 将待下线节点负责的哈希槽(及其数据)迁移到其他存活节点。
2. 确认迁移完成且数据一致后,再将该节点从集群中移除。

这个过程与扩容类似,但方向相反。Redis Cluster 提供了完善的命令支持,只要按流程操作,完全可以做到不丢数据、服务不中断


🧭 缩容前的准备:评估与规划

“磨刀不误砍柴工”,缩容前的准备工作至关重要。

🔍 1. 评估当前集群状态

连接到集群,确认整体健康。

# 连接到任意节点
redis-cli -c -h 192.168.1.101 -p 7000

# 查看集群信息
> CLUSTER INFO
cluster_state:ok
cluster_slots_assigned:16384
cluster_known_nodes:6
cluster_size:3
# ...

确保 cluster_state:ok,所有槽已分配。

📊 2. 分析节点负载与角色

使用 CLUSTER NODES 查看各节点的槽位分布和角色。

> CLUSTER NODES
c7f1f2... 192.168.1.101:7000@17000 master - 0 1678901234567 1 connected 0-5460
a3b2c1... 192.168.1.101:7001@17001 slave c7f1f2... 0 1678901234567 1 connected
d8e9f0... 192.168.1.102:7002@17002 master - 0 1678901234567 2 connected 5461-10922
f1g2h3... 192.168.1.102:7003@17003 slave d8e9f0... 0 1678901234567 2 connected
e4f5g6... 192.168.1.103:7004@17004 master - 0 1678901234567 3 connected 10923-16383
h7i8j9... 192.168.1.103:7005@17005 slave e4f5g6... 0 1678901234567 3 connected

假设我们计划下线 192.168.1.101:7000(Node ID: c7f1f2...)及其从节点 192.168.1.101:7001

💾 3. 数据备份(强烈建议)

尽管缩容是在线操作,但为防万一,执行一次 BGSAVE

# 在待下线的主节点上
redis-cli -p 7000 BGSAVE

等待 SAVE 完成,确保有最新的 RDB 快照。


🔄 步骤一:迁移待下线主节点的数据

这是缩容的核心步骤,必须确保所有数据安全迁移到其他主节点。

方法一:使用 redis-cli --cluster reshard 自动迁移(推荐)

Redis 提供了自动化工具,简化迁移流程。

redis-cli --cluster reshard 192.168.1.101:7000

交互式向导流程:

  1. How many slots do you want to move?
    输入待下线节点负责的槽数。7000 节点负责 0-5460,共 5461 个槽。输入 5461

  2. What is the receiving node ID?
    你想把数据迁移到哪个节点?可以输入一个目标节点的 node ID,也可以输入 all 让工具自动平均分配到所有其他主节点。

    为了负载均衡,推荐输入 all

  3. Source node #1:
    输入待下线节点的 node ID,即 c7f1f2...
    输入 done 结束。

  4. Type ‘yes’ to accept the plan:
    工具会生成一个迁移计划,确认无误后输入 yes

命令执行后,Redis 会开始将 5461 个槽位的数据从 7000 节点迁移到其他主节点(70027004)。

Before Shrinking
Migrates Slots 0-5460
Migrates Slots 0-5460
Now: 5461-10922 + part of 0-5460
Now: 10923-16383 + part of 0-5460
Now: No Slots
Master A: Slots 0-5460
Master B: Slots 5461-10922
Master C: Slots 10923-16383
After Data Migration

如上图所示,待下线节点 A 的槽位被迁移到 B 和 C,A 变为空壳。

方法二:手动迁移(高级控制)

如果你需要精细控制迁移过程(如分批迁移),可以手动执行。

1. 选择目标节点并设置 IMPORTING

假设将槽位 0-2730 迁移到 7002 节点(d8e9f0...)。

7002 节点上执行:

redis-cli -p 7002 CLUSTER SETSLOT 0 IMPORTING c7f1f2...
redis-cli -p 7002 CLUSTER SETSLOT 1 IMPORTING c7f1f2...
# ... 重复到 2730
redis-cli -p 7002 CLUSTER SETSLOT 2730 IMPORTING c7f1f2...
2. 在源节点设置 MIGRATING

7000 节点上执行:

redis-cli -p 7000 CLUSTER SETSLOT 0 MIGRATING d8e9f0...
redis-cli -p 7000 CLUSTER SETSLOT 1 MIGRATING d8e9f0...
# ... 重复到 2730
redis-cli -p 7000 CLUSTER SETSLOT 2730 MIGRATING d8e9f0...
3. 迁移 key

使用脚本迁移 key

# 获取槽位 0 中的 key
KEYS=$(redis-cli -p 7000 CLUSTER GETKEYSINSLOT 0 10)

for key in $KEYS; do
  # 迁移 key 到 7002
  redis-cli -p 7000 MIGRATE 192.168.1.102 7002 "$key" 0 5000
done
4. 完成槽位迁移

当所有 key 迁移完成后,更新槽位归属。

# 在任意节点执行
redis-cli -p 7000 CLUSTER SETSLOT 0 NODE d8e9f0...
redis-cli -p 7000 CLUSTER SETSLOT 1 NODE d8e9f0...
# ... 重复到 2730
redis-cli -p 7000 CLUSTER SETSLOT 2730 NODE d8e9f0...
5. 清理状态
# 在源节点
redis-cli -p 7000 CLUSTER SETSLOT 0 STABLE
# 在目标节点
redis-cli -p 7002 CLUSTER SETSLOT 0 STABLE
# 对每个槽位重复

重复以上步骤,将剩余槽位(2731-5460)迁移到 7004 节点。


🚫 步骤二:从集群中移除待下线节点

当数据迁移完成后,待下线节点已不再负责任何槽位,可以安全移除。

1. 移除从节点

首先移除从节点 7001

# 从任意集群节点执行
redis-cli --cluster del-node 192.168.1.101:7000 h7i8j9...
# 注意:第一个参数是集群中任意活跃节点,第二个是待移除节点的 node ID

或使用 CLUSTER FORGET

# 在任意其他节点上执行
redis-cli -p 7002 CLUSTER FORGET h7i8j9...
redis-cli -p 7004 CLUSTER FORGET h7i8j9...

CLUSTER FORGET <node-id> 会从当前节点的节点列表中移除指定节点。需要在集群中所有其他节点上执行,才能完全移除。

2. 移除主节点

待下线主节点 7000 在数据迁移后已无槽位,可以直接移除。

redis-cli --cluster del-node 192.168.1.102:7002 c7f1f2...

或使用 CLUSTER FORGET 在其他节点上执行。

3. 停止 Redis 进程

确认节点已从 CLUSTER NODES 列表中消失后,可以安全停止 Redis 进程。

# 在 192.168.1.101 服务器上
redis-cli -p 7000 SHUTDOWN
redis-cli -p 7001 SHUTDOWN

或者停止 Docker 容器。


🧪 缩容后的验证

操作完成后,必须进行全面验证。

1. 检查集群状态

redis-cli -c -h 192.168.1.102 -p 7002 CLUSTER INFO
# cluster_state:ok
# cluster_slots_assigned:16384
# cluster_size:2 (now 2 masters)
# cluster_known_nodes:4 (2 masters + 2 replicas)

2. 检查节点拓扑

redis-cli -c -p 7002 CLUSTER NODES
# 确认 c7f1f2... 和 h7i8j9... 已消失
# 确认剩余节点的槽位分配正确

3. 测试数据读写

redis-cli -c -p 7002
> GET user:1001
"1"
> SET product:2001 "Phone"
OK

尝试访问之前由 7000 节点负责的 key,确保能正确重定向并获取数据。


💻 Java 客户端如何应对缩容?

好消息是,使用 Lettuce 或 Jedis 等智能客户端的应用,在缩容过程中几乎无需任何改动

Lettuce 自动处理缩容

import io.lettuce.core.*;
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;

import java.util.Arrays;

public class RedisClusterShrinkClient {

    public static void main(String[] args) {
        // 提供集群中任意节点地址(无需包含已下线节点)
        RedisURI node1 = RedisURI.create("redis://192.168.1.102:7002");
        RedisURI node2 = RedisURI.create("redis://192.168.1.103:7004");

        RedisClusterClient clusterClient = RedisClusterClient.create(Arrays.asList(node1, node2));

        try {
            StatefulRedisClusterConnection<String, String> connection = clusterClient.connect();
            RedisAdvancedClusterCommands<String, String> sync = connection.sync();

            // 客户端会自动发现集群拓扑变化
            sync.set("user:1001", "Alice");
            String user = sync.get("user:1001");
            System.out.println("User: " + user);

            // 即使数据已迁移到新节点,客户端也能正确路由
            sync.set("order:2001", "Shipped");
            System.out.println("Order: " + sync.get("order:2001"));

            // 批量操作
            sync.mset("a", "1", "b", "2");
            System.out.println("MGET: " + sync.mget("a", "b"));

            connection.close();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            clusterClient.shutdown();
        }
    }
}

工作原理

  • Lettuce 客户端维护一个本地的槽位映射表。
  • 当访问一个 key 时,如果收到 MOVED 响应(指向新节点),客户端会更新本地映射表。
  • 下次请求相同槽位的 key 时,客户端直接连接新节点,避免重定向。
  • 如果客户端尝试连接已下线的节点,会触发拓扑刷新,重新从集群获取最新节点列表。

Jedis 同样支持

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;

import java.util.HashSet;
import java.util.Set;

public class JedisShrinkClient {

    public static void main(String[] args) {
        Set<HostAndPort> nodes = new HashSet<>();
        nodes.add(new HostAndPort("192.168.1.102", 7002));
        nodes.add(new HostAndPort("192.168.1.103", 7004));

        try (JedisCluster cluster = new JedisCluster(nodes)) {
            cluster.set("key1", "value1");
            System.out.println("key1: " + cluster.get("key1"));

            // Jedis 会自动处理 MOVED 重定向和节点变更
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

📚 延伸阅读


🎉 总结

Redis 集群缩容并不可怕,关键在于遵循正确的流程:

  1. 先迁移数据:使用 --cluster reshard 将待下线节点的槽位和数据迁移到其他主节点。
  2. 再移除节点:使用 --cluster del-nodeCLUSTER FORGET 将节点从集群中移除。
  3. 最后停止服务:确认无误后,关闭 Redis 进程。

只要操作规范,配合智能客户端,缩容完全可以做到零数据丢失、服务不中断。希望本文能帮你克服“怕丢数据”的心理障碍,从容应对资源调整,让 Redis 集群的运维更加灵活高效。🎉


🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐