Redis 集群迁移方案:从三节点到三节点的无缝过渡

目录

一、背景与挑战

迁移需求

核心挑战

二、迁移架构设计

[2.1 为什么需要"一主五从六哨兵"?](#2.1 为什么需要"一主五从六哨兵"?)

[2.2 数据流向设计](#2.2 数据流向设计)

三、迁移脚本设计解析

[3.1 交互式安全设计](#3.1 交互式安全设计)

[3.2 迁移流程(六步法)](#3.2 迁移流程(六步法))

[3.3 智能拓扑发现](#3.3 智能拓扑发现)

四、关键技术实现

[4.1 无感知故障转移](#4.1 无感知故障转移)

[4.2 状态验证机制](#4.2 状态验证机制)

五、可行性分析

[5.1 风险评估与缓解](#5.1 风险评估与缓解)

[5.2 性能影响分析](#5.2 性能影响分析)

[5.3 回退方案](#5.3 回退方案)

六、实施建议

[6.1 前置检查清单](#6.1 前置检查清单)

[6.2 最佳实践](#6.2 最佳实践)

七、总结


一、背景与挑战

迁移需求

将 Redis 集群从三台老服务器(10.10.10.36-38)迁移到三台新服务器(10.10.10.180-182),涉及 21 个 Redis 实例(端口 20001-20023)和一个哨兵端口(30001)。

核心挑战

  • 零数据丢失:确保迁移过程中数据完整性。

  • 最小化业务影响:控制单次故障转移时间在秒级。

  • 平滑过渡:无需修改客户端配置,由哨兵自动完成服务发现。

  • 可回退性:任何步骤失败都能快速回退到原始状态。

二、迁移架构设计

2.1 为什么需要"一主五从六哨兵"?

在正式迁移前,我们构建了一个过渡架构

bash 复制代码
原始架构:
老集群(3节点) → 新集群(3节点)

过渡架构:
老集群(3节点) + 新集群(3节点) → 6节点混合集群

设计考量:

  1. 平滑接替:新服务器作为老集群的从节点加入,通过复制同步全量数据。

  2. 快速故障转移:6 个哨兵节点保证 quorum,避免脑裂。

  3. 双向可见:客户端可通过任意哨兵发现整个集群拓扑。

  4. 渐进式迁移:逐个实例迁移,降低风险。

2.2 数据流向设计

bash 复制代码
迁移前:Client → 哨兵 → 老Master → 老Slaves
迁移中:Client → 哨兵 → 新Master ← 同步 → 老Slaves
迁移后:Client → 哨兵 → 新Master → 新Slaves

三、迁移脚本设计解析

迁移脚本 migrate_redis.sh 内容如下:

bash 复制代码
#!/bin/bash
# migrate_redis.sh
 
# 执行这个脚本前,先关闭所有三个老哨兵并确认
# /home/redis/redis-5.0.3/src/redis-cli -p 30001 shutdown
# ps -ef | grep sentinel
 
if [ $# -ne 2 ]; then
    echo "执行方式:$0 redis端口号 口令"
    echo "示例:$0 20001 password123"
    exit 1
fi
 
PORT=$1
PASSWORD=$2
REDIS_CLI="/home/redis/redis-5.0.3/src/redis-cli"
 
get_char() {
    SAVEDSTTY=`stty -g`
    stty -echo
    stty cbreak
    dd if=/dev/tty bs=1 count=1 2> /dev/null
    stty -raw
    stty echo
    stty $SAVEDSTTY
}
 
echo "========================================"
echo "开始迁移 Redis 实例:端口 $PORT"
echo "========================================"
 
# 1. 从哨兵获取 master 信息
echo -e "\n[1/6] 从哨兵获取主节点信息..."
echo -e "\n按任意键继续,Ctrl+C 终止...\n"
char=`get_char`
 
line=`$REDIS_CLI -h 10.10.10.180 -p 30001 info | grep $PORT`
 
if [ -z "$line" ]; then
    echo "⚠:哨兵中未找到端口 $PORT 的监控信息"
    exit 1
fi
 
# 2. 提取信息
master_ip=`echo $line | awk -F, '{print $3}' | awk -F: '{print $1}' | awk -F= '{print $2}'`
master_ip_last_part=`echo $master_ip | awk -F. '{print $4}'`
let slave_ip_last_part1=($master_ip_last_part-36+1)%3+36
let slave_ip_last_part2=($master_ip_last_part-36+2)%3+36
slave_ip1=10.10.10.$slave_ip_last_part1
slave_ip2=10.10.10.$slave_ip_last_part2
master_name=`echo $line | awk -F"," '{print $1}' | awk -F"=" '{print $2}'`
 
echo "✅ 主节点: $master_ip:$PORT"
echo "✅ 哨兵组: $master_name"
echo "✅ 从节点: $slave_ip1:$PORT, $slave_ip2:$PORT"
 
# 检查主节点是否在老服务器
if [[ ! "$master_ip" =~ ^10\.10\.10\.(36|37|38)$ ]]; then
    echo -e "\n⚠ 主节点 $master_ip 已在新服务器,无需迁移"
    exit 0
fi
 
# 3. 关闭 slave 实例
echo -e "\n[2/6] 关闭从节点"
echo "----------------------------------------"
echo "将关闭以下从节点:"
echo "  • $slave_ip1:$PORT"
echo "  • $slave_ip2:$PORT"
echo -e "\n按任意键继续,Ctrl+C 终止...\n"
char=`get_char`
 
for slave_ip in $slave_ip1 $slave_ip2; do
    echo -n "关闭 $slave_ip:$PORT ... "
    if $REDIS_CLI -h $slave_ip -p $PORT -a $PASSWORD ping 2>/dev/null | grep -q "PONG"; then
        $REDIS_CLI -h $slave_ip -p $PORT -a $PASSWORD SHUTDOWN NOSAVE 2>/dev/null && echo "✅" || echo "❌"
    else
        echo "⚠ 未运行"
    fi
done

# 等待从节点上 redis-server 进程终止
sleep 3

# 4. 重置哨兵
echo -e "\n[3/6] 重置哨兵配置"
echo "----------------------------------------"
echo "哨兵组名: $master_name"
echo "重置所有新服务器上的哨兵监控信息..."
echo -e "\n按任意键继续,Ctrl+C 终止...\n"
char=`get_char`
 
for server in 10.10.10.180 10.10.10.181 10.10.10.182; do
    echo -n "重置哨兵 $server ... "
    $REDIS_CLI -h $server -p 30001 sentinel reset $master_name 2>/dev/null && echo "✅" || echo "❌"
    echo -n "等待哨兵同步 "
    for i in {1..30}; do
        if $REDIS_CLI -h $server -p 30001 info 2>/dev/null | grep "$master_name" | grep -q "slaves=3,sentinels=3"; then
            echo "✅ (耗时 ${i}秒)"
            break
        fi
        if [ $i -eq 30 ]; then
            echo "❌ (等待超时)"
            echo "错误:哨兵 $server 在30秒内未完成同步"
            echo "请检查哨兵状态和网络连接"
            exit 1
        fi
        echo -n "."
        sleep 1
    done
    $REDIS_CLI -h $server -p 30001 info | grep $PORT
done
 
# 5. 关闭 master 实例
echo -e "\n[4/6] 关闭主节点,触发故障转移"
echo "----------------------------------------"
echo "主节点: $master_ip:$PORT"
echo -e "操作流程:"
echo "  1. 暂停客户端写入 3000ms"
echo "  2. 立即关闭主节点"
echo "  3. 等待哨兵自动选举新主节点"
echo -e "\n预计业务中断时间:~11秒"
echo -e "\n按任意键继续,Ctrl+C 终止...\n"
char=`get_char`
 
echo -n "暂停客户端写入 ... "
$REDIS_CLI -h $master_ip -p $PORT -a $PASSWORD CLIENT PAUSE 3000 2>/dev/null && echo "✅" || echo "❌"
sleep 3
echo -n "关闭主节点 ... "
$REDIS_CLI -h $master_ip -p $PORT -a $PASSWORD SHUTDOWN NOSAVE 2>/dev/null && echo "✅" || echo "❌"
 
# 6. 等待故障转移(动态判断)
echo -e "\n[5/6] 等待故障转移完成"
echo "----------------------------------------"
echo -n "等待故障转移 "

for i in {1..30}; do
    # 收集所有哨兵返回的主节点信息
    MASTER_INFO=()
    MASTER_COUNT=0
    
    for server in 10.10.10.180 10.10.10.181 10.10.10.182; do
        result=$($REDIS_CLI -h $server -p 30001 sentinel get-master-addr-by-name $master_name 2>/dev/null)
        if [[ -n "$result" ]] && [[ "$result" != "NULL" ]]; then
            # 正确处理哨兵返回的结果(包含换行符)
            MASTER_INFO+=("$server:$result")
            MASTER_COUNT=$((MASTER_COUNT + 1))
        fi
    done
    
    # 检查是否三个哨兵都返回了相同的主节点
    if [ $MASTER_COUNT -eq 3 ]; then
        # 提取所有主节点信息进行对比
        MASTER_VALUES=()
        for info in "${MASTER_INFO[@]}"; do
            # 提取哨兵返回的结果部分(去掉服务器前缀)
            # 注意:info格式是 "哨兵IP:主节点IP\n端口"
            # 我们需要完整提取哨兵返回的值(包括换行)
            master_value="${info#*:}"  # 去掉哨兵IP部分
            
            # 将多行结果转换为单行便于比较,格式:ip:port
            ip=$(echo "$master_value" | head -n1)
            port=$(echo "$master_value" | tail -n1)
            MASTER_VALUES+=("$ip:$port")
        done
        
        # 检查所有哨兵返回的主节点是否一致
        UNIQUE_MASTERS=$(printf "%s\n" "${MASTER_VALUES[@]}" | sort -u | wc -l)
        
        if [ "$UNIQUE_MASTERS" -eq 1 ]; then
            NEW_MASTER="${MASTER_VALUES[0]}"
            new_ip=$(echo "$NEW_MASTER" | cut -d: -f1)
            
            # 检查是否在新服务器上
            if [[ "$new_ip" =~ ^10\.10\.10\.(180|181|182)$ ]]; then
                echo "✅ (耗时 ${i}秒)"
                echo "新主节点: $NEW_MASTER"
                echo "哨兵一致性:3/3 ✅"
                break
            else
                # 如果三个哨兵一致但不在新服务器上
                if [ $i -eq 30 ]; then
                    echo "❌ (等待超时)"
                    echo "错误:三个哨兵一致选举的主节点仍在老服务器: $NEW_MASTER"
                    echo "请手动检查故障转移状态"
                    exit 1
                fi
            fi
        else
            # 如果哨兵返回不一致
            if [ $i -eq 30 ]; then
                echo "❌ (等待超时)"
                echo "错误:哨兵返回的主节点信息不一致"
                echo "详细信息:"
                for ((idx=0; idx<3; idx++)); do
                    server_info="${MASTER_INFO[$idx]}"
                    server="${server_info%%:*}"
                    echo "  哨兵 $server: ${MASTER_VALUES[$idx]}"
                done
                exit 1
            fi
        fi
    elif [ $MASTER_COUNT -eq 0 ]; then
        # 没有哨兵返回有效信息
        if [ $i -eq 30 ]; then
            echo "❌ (等待超时)"
            echo "错误:30秒内无哨兵返回主节点信息"
            echo "请检查哨兵服务是否正常"
            exit 1
        fi
    else
        # 部分哨兵返回信息(1或2个)
        if [ $i -eq 30 ]; then
            echo "❌ (等待超时)"
            echo "错误:只有 $MASTER_COUNT/3 个哨兵返回主节点信息"
            for ((idx=0; idx<MASTER_COUNT; idx++)); do
                server_info="${MASTER_INFO[$idx]}"
                server="${server_info%%:*}"
                master_value="${server_info#*:}"
                ip=$(echo "$master_value" | head -n1)
                port=$(echo "$master_value" | tail -n1)
                echo "  哨兵 $server: $ip:$port"
            done
            exit 1
        fi
    fi
    
    # 非超时情况下继续等待
    echo -n "."
    sleep 1
done
 
# 7. 再次重置哨兵
echo -e "\n[6/6] 清理哨兵状态"
echo "----------------------------------------"
echo -e "\n按任意键继续,Ctrl+C 终止...\n"
char=`get_char`
 
for server in 10.10.10.180 10.10.10.181 10.10.10.182; do
    echo -n "重置哨兵 $server ... "
    $REDIS_CLI -h $server -p 30001 sentinel reset $master_name 2>/dev/null && echo "✅" || echo "❌"
    echo -n "等待哨兵同步 "
    for i in {1..30}; do
        if $REDIS_CLI -h $server -p 30001 info 2>/dev/null | grep "$master_name" | grep -q "slaves=2,sentinels=3"; then
            echo "✅ (耗时 ${i}秒)"
            break
        fi
        if [ $i -eq 30 ]; then
            echo "❌ (等待超时)"
            echo "错误:哨兵 $server 在30秒内未完成同步"
            echo "请检查哨兵状态和网络连接"
            exit 1
        fi
        echo -n "."
        sleep 1
    done
done
 
# 8. 显示结果
echo -e "\n========================================"
echo "迁移完成!验证结果:"
echo "========================================"
echo -e "\n按任意键继续,Ctrl+C 终止...\n"
char=`get_char`
 
# 再次获取新主节点
NEW_MASTER=""
for server in 10.10.10.180 10.10.10.181 10.10.10.182; do
    result=$($REDIS_CLI -h $server -p 30001 sentinel get-master-addr-by-name $master_name 2>/dev/null)
    if [[ -n "$result" ]] && [[ "$result" != "NULL" ]]; then
        NEW_MASTER="$result"
        break
    fi
done
 
if [[ -n "$NEW_MASTER" ]]; then
    echo "✅ 新主节点: $NEW_MASTER"
    
    # 检查是否迁移到新服务器
    new_ip=$(echo "$NEW_MASTER" | awk '{print $1}')
    if [[ "$new_ip" =~ ^10\.10\.10\.(180|181|182)$ ]]; then
        echo "✅ 已成功迁移到新服务器"
    else
        echo "⚠ 新主节点仍在老服务器: $new_ip"
    fi
else
    echo "⚠ 未能获取新主节点信息"
fi
 
check_redis_server() {
    local server=$1
    
    echo -n "$server:$PORT ... "
    
    # 获取信息,使用--raw参数
    local output
    output=$($REDIS_CLI -h "$server" -p $PORT -a $PASSWORD --raw info replication 2>/dev/null)
    
    if [ $? -eq 0 ] && [ -n "$output" ]; then
        # 提取角色
        local role
        role=$(echo "$output" | awk -F: '/^role:/ {print $2}' | tr -d '[:space:]')
        
        if [ -n "$role" ]; then
            echo "$role"
            
            case "$role" in
                slave)
                    # 显示slave的master信息
                    echo "$output" | awk -F: '
                    /^master_host:/ {printf "  %-25s %s\n", "master_host:", $2}
                    /^master_port:/ {printf "  %-25s %s\n", "master_port:", $2}
                    /^master_link_status:/ {printf "  %-25s %s\n", "master_link_status:", $2}
                    /^master_last_io_seconds_ago:/ {printf "  %-25s %s\n", "master_last_io_seconds_ago:", $2}
                    /^master_sync_in_progress:/ {printf "  %-25s %s\n", "master_sync_in_progress:", $2}
                    '
                    ;;
                master)
                    # 显示master的slave信息
                    local slaves
                    slaves=$(echo "$output" | awk -F: '/^connected_slaves:/ {print $2}' | tr -d '[:space:]')
                    echo "  connected_slaves: $slaves"
                    
                    # 显示每个slave的详细信息
                    echo "$output" | grep "^slave[0-9]*:" | while read -r line; do
                        echo "  $line"
                    done
                    ;;
                *)
                    echo "  未知角色"
                    ;;
            esac
        else
            echo "运行中(无法解析角色)"
        fi
    else
        echo "未运行"
    fi
}
 
echo -e "\n快速检查:"
echo -e "\n按任意键继续,Ctrl+C 终止...\n"
char=`get_char`
 
for server in 10.10.10.180 10.10.10.181 10.10.10.182; do
    $REDIS_CLI -h "$server" -p 30001 info | grep $PORT
done
 
echo
 
for server in 10.10.10.180 10.10.10.181 10.10.10.182; do
    check_redis_server "$server"
    echo ""
done
 
echo "========================================"

3.1 交互式安全设计

bash 复制代码
# 关键安全特性
1. 逐步骤确认:每个危险操作前需要手动确认
2. 状态检查:执行前验证哨兵监控信息
3. 超时控制:CLIENT PAUSE 3000ms保证写入缓冲
4. 过程可视化:实时显示倒计时和状态

3.2 迁移流程(六步法)

bash 复制代码
1. 获取拓扑信息:从哨兵识别主从关系
2. 关闭从节点:有序下线,保留数据一致性
3. 重置哨兵:清理老节点监控信息
4. 主节点切换:CLIENT PAUSE + SHUTDOWN触发故障转移
5. 等待选举:10秒等待哨兵完成新主选举
6. 状态验证:确认新主节点在新服务器

3.3 智能拓扑发现

bash 复制代码
# 自动计算主从关系
let slave_ip_last_part1=($master_ip_last_part-36+1)%3+36
let slave_ip_last_part2=($master_ip_last_part-36+2)%3+36

# 算法逻辑:
# 36 → slaves: 37, 38
# 37 → slaves: 38, 36  
# 38 → slaves: 36, 37

四、关键技术实现

4.1 无感知故障转移

bash 复制代码
# 关键操作序列
CLIENT PAUSE 3000      # 暂停客户端写入
SHUTDOWN NOSAVE        # 不保存立即关闭
sentinel reset         # 清理哨兵状态

4.2 状态验证机制

bash 复制代码
# 多维度验证
1. 哨兵视角:sentinel get-master-addr-by-name
2. Redis视角:info replication
3. 网络视角:ping检测
4. 角色验证:确认master/slave状态

五、可行性分析

5.1 风险评估与缓解

风险点 可能性 影响 缓解措施
故障转移失败 10秒超时回退,人工介入
数据不一致 主从同步验证,CLIENT PAUSE保证
哨兵分裂 极低 6哨兵quorum=4,必须多数同意
网络分区 双向连通性测试前置检查

5.2 性能影响分析

  • 内存使用:6节点期间内存占用翻倍(临时)。

  • 网络带宽:初始全量同步消耗较大,但分批迁移可控制。

  • 客户端影响:单实例11秒不可用,整体服务可用性99.9%+。

5.3 回退方案

  1. 快速回退:重启老节点,哨兵自动恢复。

  2. 数据回退:新节点作为从节点重新同步老集群。

  3. 客户端回退:客户端配置不变,由哨兵管理。

六、实施建议

6.1 前置检查清单

bash 复制代码
# 必须验证的项目
1. 网络连通性:老新服务器双向可达
2. 哨兵配置:所有节点monitor配置一致
3. 密码验证:所有实例密码统一
4. 版本兼容:Redis 5.0.3版本一致
5. 资源充足:新服务器内存/磁盘充足

6.2 最佳实践

  1. 分批迁移:按业务重要性分批,先非关键业务。

  2. 监控预警:迁移期间加强Redis监控。

  3. 业务低峰:选择业务流量最低时段。

  4. 文档记录:详细记录每个实例迁移状态。

  5. 应急预案:准备完整的回退流程。

七、总结

本迁移方案通过过渡架构设计交互式脚本控制,实现了Redis集群的无缝迁移。核心优势:

  1. 安全性:逐步骤确认,防止误操作。

  2. 可靠性:基于哨兵标准故障转移机制。

  3. 可控性:单实例控制,风险隔离。

  4. 透明性:客户端无需配置变更。

该方案已在生产环境验证,单实例迁移时间约11秒,数据零丢失,业务影响最小化,是Redis集群迁移的可靠实践方案。

技术栈 :Redis 5.0.3 + Sentinel + Bash脚本
迁移规模 :21个实例 + 6哨兵
设计原则:安全第一、渐进过渡、可监控、可回退

相关推荐
墨笔之风1 小时前
MySQL与PostgreSQL选型对比及适用场景说明
数据库·mysql·postgresql
肥大毛1 小时前
Oracle中Merge Using用法
数据库·oracle
程序员Agions2 小时前
N+1 查询:那个让你 API 慢成 PPT 的隐形杀手
数据库·后端
banjin2 小时前
轻量化时序数据库新选择:KaiwuDB-Lite 实战体验
数据库·oracle·边缘计算·时序数据库·kaiwudb·kwdb
阳光九叶草LXGZXJ2 小时前
达梦数据库-学习-43-定时备份模式和删除备份(Python+Crontab)
linux·运维·开发语言·数据库·python·学习
DemonAvenger2 小时前
Redis监控系统搭建:关键指标与预警机制实现
数据库·redis·性能优化
_F_y2 小时前
数据库基础
数据库·adb
zgl_200537792 小时前
源代码:ZGLanguage 解析SQL数据血缘 之 显示 UNION SQL 结构图
大数据·数据库·数据仓库·sql·数据治理·sql解析·数据血缘
柚几哥哥2 小时前
Redis 优化实践:高性能设备缓存系统设计
数据库·redis·缓存