Redis 数据迁移与集群扩容:从单节点到分布式的大规模迁移实战

Redis 生产环境数据迁移完整指南:单节点升集群、在线热迁移、Slot 重分片、BigKey 治理、双写一致性校验、零停机切换与 K8s 部署

Redis 作为内存数据库,在生产环境的生命周期中必然面临各种迁移需求:业务增长推动单节点升级为集群架构、机房搬迁需要跨数据中心迁移、服务器老化触发硬件替换,或是云原生转型需要接入 Kubernetes 部署。每一次迁移都牵一发而动全身,数据完整性、服务可用性、迁移效率三者必须同时满足。本文从真实生产场景出发,系统梳理 Redis 数据迁移的九大核心主题,涵盖工具选型、Slot 重分片、BigKey 治理、一致性校验与零停机切换的完整操作流程。

一、Redis 迁移场景分类与选型

1.1 常见迁移场景

生产环境中的 Redis 迁移通常分为三大类:

架构升级:单体应用演进为分布式系统时,单节点 Redis 无法满足容量和并发需求,需要迁移到 Redis Cluster。这是最典型也最复杂的迁移类型,涉及数据分片、Hash Tag 适配和客户端路由改造。

容量扩展:现有 Cluster 集群的某些节点负载过高,需要增加节点重新分配槽位。这种场景相对简单,因为客户端通常无需改动,只需在集群层面执行槽迁移。

跨机房/跨云迁移:包括同城双活、异地灾备、云厂商切换等。这类迁移对网络延迟最敏感,需要在带宽、一致性和切换时间之间取舍。

1.2 迁移工具对比

工具适用场景增量同步断点续传开源状态
redis-migrate-tool单节点/主从 -> Cluster已停止维护
redis-shake (v2/v3)全场景支持阿里云持续维护
redis-port各种架构间迁移已停止维护
MIGRATE 命令小规模、特定 Key原生支持
CDC (Change Data Capture)与业务结合的双写场景自建方案

工具选型建议:Redis 6.x 及之前版本优先使用 redis-shake v2,Redis 7.x 使用 redis-shake v3。redis-migrate-tool 虽然原理清晰,但自 2017 年后停止更新,不支持增量同步和断点续传,仅适合一次性小规模迁移。MIGRATE 命令在 Cluster 模式下只能迁移非槽位 Key,场景受限。

二、在线热迁移:redis-shake 实战

redis-shake 是阿里云开源的 Redis 数据迁移工具,支持全量同步、增量同步、断点续传,是目前生产环境的首选方案。它基于 RDB 快照解析 + AOF 命令回放实现数据同步。

2.1 安装与配置

# 下载 redis-shake v3(支持 Redis 7.x)
wget https://github.com/alibaba/RedisShake/releases/download/v3.1.11/redis-shake-linux-amd64.tar.gz
tar -xzf redis-shake-linux-amd64.tar.gz
chmod +x redis-shake

redis-shake 使用 TOML 配置文件,下面是一个典型的 单节点 -> Cluster 迁移配置:

[sync_reader]
address = "192.168.1.10:6379"
password = "source_password"

[cluster_shake_writer]
address = ["192.168.2.10:6379", "192.168.2.11:6379", "192.168.2.12:6379"]
password = "target_password"
tls = false

[filter]
# 可选:只迁移特定模式的 Key
key_exists = "none"
# 比如只迁移用户会话数据
# key_prefix = ["session:", "user:"]

2.2 全量 + 增量同步流程

redis-shake 的同步过程分为三个阶段:

  1. 全量同步(Full Sync):连接源端执行 PSYNCSYNC,接收并解析 RDB 文件,批量写入目标端。对于 Cluster 目标端,shake 会自动根据 Key 的 CRC16 值计算所属槽位,路由到正确的节点。

  2. 增量同步(Continue):全量完成后,进入实时命令回放模式。源端的每一条写命令都通过源端发送的 AOF 流直接转发到目标端。

  3. 优雅的断开:当需要切换业务流量时,先停止源端写入(或进入只读模式),确认增量延迟为 0 后,停止 redis-shake。

# 启动迁移(后台运行建议配合 nohup/systemd)
./redis-shake sync.toml

# 监控同步状态
# 日志中会输出 processed_bytes、total_bytes、bps 等指标
tail -f redis-shake.log | grep -E "processed|total|bps|sync"

2.3 断点续传与异常恢复

redis-shake v3 支持断点续传。当迁移过程中断(网络闪断、进程重启),重启后会读取本地保存的 RDB 解析进度,仅重新同步断点后的增量数据,无需从头开始全量同步。

# 断点信息保存在本地 workdir 目录
ls -la ./data/
# 包含 parse.offset、sync.offset 等文件

常见异常处理

  • 目标端内存不足(OOM):redis-shake 会持续重试写入,直到目标端触发 maxmemory-policy 淘汰策略。迁移前应确保目标端预留足够的内存空间(建议源端数据的 1.5 倍以上)。
  • 网络带宽不足:使用 --pipeline 参数提高批处理效率,或在非业务高峰期执行全量同步。
  • BigKey 导致超时:redis-shake 默认对单个 Key 的写入有超时限制。遇到超大 Hash 或 ZSet 时,可能需要拆分 Key 或调整目标端的 proto-max-bulk-len

2.4 原生 MIGRATE 命令

对于小规模数据迁移,或需要精确控制单个 Key 的场景,Redis 原生的 MIGRATE 命令是最直接的选择。

# 将源端的 user:1001 迁移到目标端
MIGRATE 192.168.2.10 6379 user:1001 0 5000 AUTH target_password

# 批量迁移匹配前缀的所有 Key(配合 shell 脚本)
redis-cli --scan --pattern "session:*" | while read key; do
  redis-cli MIGRATE 192.168.2.10 6379 "$key" 0 5000 COPY AUTH target_password
done

注意:Cluster 模式下,MIGRATE 只能迁移属于当前节点负责的槽位的 Key。如果要跨节点迁移,必须通过客户端连接正确的源节点。因此 MIGRATE 在 Cluster 场景下远不如 redis-shake 便捷。

三、Cluster 模式下的 Slot 迁移与重平衡

3.1 槽位(Slot)迁移原理

Redis Cluster 将 16384 个槽位分配到各主节点。扩容时新增主节点,需要从现有节点迁移部分槽位;缩容时则需要将待下线节点的槽位全部分配给其他节点再执行移除。

槽迁移是 Redis Cluster 的内建能力,整个过程在槽粒度上完成,对业务的影响极小。核心机制如下:

  1. 目标节点向源节点发送 CLUSTER SETSLOT <slot> IMPORTING <source_node_id>
  2. 源节点向目标节点发送 CLUSTER SETSLOT <slot> MIGRATING <target_node_id>
  3. 源节点遍历该槽位下的所有 Key,使用 MIGRATE 命令批量迁移到目标节点。
  4. 所有 Key 迁移完成后,双方节点广播槽位归属变更。

迁移期间,属于正在迁移槽位的查询会被重定向(MOVED/ASK 响应),客户端需要正确处理重定向。

3.2 redis-cli –cluster reshard

这是官方推荐的手动重分片命令,适合可控的小规模调整:

# 执行重分片(交互式)
redis-cli --cluster reshard 192.168.1.10:6379

# 非交互式:将 2000 个槽位从源节点迁出,分配给目标节点
redis-cli --cluster reshard 192.168.1.10:6379 \
  --cluster-from <source_node_id> \
  --cluster-to <target_node_id> \
  --cluster-slots 2000 \
  --cluster-yes

参数说明:

  • --cluster-from:源节点 ID,多个用逗号分隔,或输入 all 从所有现有节点均匀迁出。
  • --cluster-to:目标节点 ID(迁移目的地)。
  • --cluster-slots:要迁移的槽位数量。
  • --cluster-yes:跳过确认提示,自动化脚本使用。

3.3 自动重平衡

当新增多个节点或缩容时,手动逐个 reshard 非常繁琐。redis-cli 提供了自动重平衡功能,将所有槽位按节点数量均匀分配:

# 自动重平衡所有主节点
redis-cli --cluster rebalance 192.168.1.10:6379 \
  --cluster-use-empty-masters

# 指定权重(某些节点配置更高,承担更多槽位)
redis-cli --cluster rebalance 192.168.1.10:6379 \
  --cluster-weight <node_id1>=5,<node_id2>=3,<node_id3>=2

--cluster-use-empty-masters 的含义是将没有任何槽位的主节点也纳入重平衡计算中。新加入的节点默认没有槽位,只有加上这个参数,rebalance 才会将槽位分配给它们。

3.4 扩容缩容完整流程

新增节点扩容

# 1. 启动新 Redis 实例并加入集群
redis-cli --cluster add-node 192.168.1.20:6379 192.168.1.10:6379

# 2. 执行自动重平衡
redis-cli --cluster rebalance 192.168.1.10:6379 --cluster-use-empty-masters

# 3. 为新增主节点配置从节点(可选但强烈建议)
redis-cli --cluster add-node 192.168.1.21:6379 192.168.1.10:6379 \
  --cluster-slave --cluster-master-id <new_master_id>

缩容移除节点

# 1. 将目标节点的所有槽位迁出
redis-cli --cluster reshard 192.168.1.10:6379 \
  --cluster-from <node_to_remove_id> \
  --cluster-to <existing_master_id> \
  --cluster-slots 4096 --cluster-yes

# 2. 重复执行直到该节点槽位为 0
# 3. 移除节点
redis-cli --cluster del-node 192.168.1.10:6379 <node_to_remove_id>

3.5 迁移期间的影响与缓解

槽迁移期间,对属于该槽位的 Key 的操作会触发 ASK 重定向。如果客户端没有正确处理 ASKING 命令,可能出现请求失败。

ASK 1337 192.168.1.20:6379

客户端处理要求

  1. 收到 -ASK <slot> <ip>:<port> 回复后,向目标节点发送 ASKING 命令。
  2. 然后重新发送原命令。
  3. 后续对该槽位的请求可以直接路由到新节点(配合 MOVED 更新路由表)。

主流客户端(jedis、go-redis、redis-py-cluster)都内置了重定向处理逻辑,通常无需手动干预。但务必在低峰期执行大规模槽迁移,避免重定向风暴。

四、BigKey 迁移的挑战与解决方案

4.1 BigKey 的危害

BigKey(单 Key 占用大量内存)是 Redis 迁移中最危险的雷区。常见的 BigKey 类型包括:

  • 百万级成员的 Hash 或 ZSet
  • 巨型 String(序列化后的 JSON 数组,几十 MB)
  • 超长 List(消息队列失控堆积)

BigKey 在迁移过程中会导致:

  • MIGRATE 命令阻塞源节点和目标节点的单线程,导致正常请求被挂起。
  • RDB 生成和传输时间极长,全量同步迟迟无法完成。
  • DEL 命令删除 BigKey 时,释放大内存产生阻塞(Redis 4.0+ UNLINK 可缓解)。
  • 集群槽迁移时,单个 BigKey 导致整个槽位迁移超时失败。

4.2 BigKey 扫描与识别

迁移前必须全面扫描 BigKey。redis-cli 提供了专门的扫描命令:

# 扫描每个数据类型的 Top N 大 Key(内存占用)
redis-cli --bigkeys

# 精确扫描:使用 rdb-tools 分析 RDB 文件
pip install rdbtools python-lzf
rdb -c memory /var/lib/redis/dump.rdb > memory.csv
# 按 size_in_bytes 排序,找出占用最大的 Key
sort -t, -k4 -nr memory.csv | head -50

Go 编写的实时扫描脚本

package main

import (
	"context"
	"fmt"
	"github.com/redis/go-redis/v9"
)

func scanBigKeys(ctx context.Context, rdb *redis.Client) error {
	var cursor uint64
	for {
		keys, next, err := rdb.Scan(ctx, cursor, "*", 100).Result()
		if err != nil {
			return err
		}
		for _, key := range keys {
			size, _ := rdb.MemoryUsage(ctx, key).Result()
			if size > 1024*1024 { // 大于 1MB
				fmt.Printf("BIGKEY: %s, size: %d bytes\n", key, size)
			}
		}
		cursor = next
		if cursor == 0 {
			break
		}
	}
	return nil
}

4.3 BigKey 拆分策略

识别出 BigKey 后,迁移前必须治理。常用的拆分策略:

Hash 拆分:将大 Hash 按业务维度拆分为多个小 Hash,或者用 String 序列化替代。

# 改造前:一个 Hash 存储所有用户配置
HGETALL user:1000:config   # 返回 10万 个字段

# 改造后:按模块拆分
HGETALL user:1000:basic
HGETALL user:1000:security
HGETALL user:1000:preference

List/Set 分片:按时间窗口或 ID 范围分片。

# 改造前:消息队列全放入一个 List
LPUSH messages $data

# 改造后:按小时/天分桶
LPUSH messages:20250816_14 $data

ZSet 分段:如果 ZSet 按时间排序,可以按月份分段存储。

对于确实无法拆分的 BigKey,迁移时使用 DUMP/RESTORE 替代 MIGRATE 以获得更细粒度的控制:

# 分片恢复大 Key(需要客户端配合分片读取)
redis-cli --eval split_bigkey.lua bigkey target_host target_port
-- split_bigkey.lua
-- 将大 Hash 拆分为小 Hash 后迁移
local key = KEYS[1]
local target = ARGV[1]
local port = tonumber(ARGV[2])

local cursor = "0"
local batch = {}
repeat
    local res = redis.call('HSCAN', key, cursor, 'COUNT', 100)
    cursor = res[1]
    for i = 1, #res[2], 2 do
        batch[res[2][i]] = res[2][i+1]
    end
    if #batch >= 1000 or cursor == "0" then
        -- 分批写入目标端(此处简化,实际应使用 MIGRATE 或 RESTORE)
        for k, v in pairs(batch) do
            redis.call('HSET', key .. ":shard:" .. i, k, v)
        end
        batch = {}
    end
until cursor == "0"

五、双写与影子流量模式

5.1 双写(Dual Write)模式

双写是实现零停机迁移的核心模式:在切换流量前,同时向源端和目标端写入数据,确保两边的数据始终保持一致。

                    +------------+
      写请求  ----->| 业务应用  |
      读请求  <-----|            |
                    +------+-----+
                           |
                    +------+------+
                    |             |
               +----v---+  +-----v----+
               | 源 Redis |  | 目标 Redis |
               | (旧环境) |  | (新环境)   |
               +----------+  +------------+

双写的代码实现示例(Go):

type DualWriter struct {
	Source *redis.Client
	Target *redis.Client
	Mode   string // "sync", "async", "shadow"
}

func (d *DualWriter) Set(ctx context.Context, key string, val interface{}) error {
	// 1. 写入源端(必须成功)
	err := d.Source.Set(ctx, key, val, 0).Err()
	if err != nil {
		return err
	}

	// 2. 异步写入目标端(失败不阻塞主流程)
	if d.Mode == "async" {
		go func() {
			_ = d.Target.Set(ctx, key, val, 0).Err()
		}()
	} else if d.Mode == "sync" {
		_ = d.Target.Set(ctx, key, val, 0).Err()
	}
	return nil
}

三种双写模式

模式原理风险适用阶段
同步双写同时写两端,均成功才返回目标端延迟影响源端,或部分失败导致不一致切换前夕
异步双写源端成功即返回,目标端后台写入切换瞬间可能丢失最近几秒的异步数据迁移预热期
影子流量读请求同时发往两端,只有源端响应不写入目标端,仅用于对比延迟和正确性压测验证期

5.2 双写的数据一致性问题

双写最大的风险是两边数据不一致。常见的陷阱:

  1. 超时写:目标端写入超时但源端已成功。应用如果单纯重试,可能导致源端重复写入。应使用幂等性设计,或在重试前确认目标端状态。

  2. 部分失败:批量命令(Pipeline)中部分成功。使用 MULTI/EXEC 事务保证原子性,但事务在 Cluster 模式下要求所有 Key 位于同一槽位。

  3. 时序依赖:依赖先读后写的逻辑在双写环境下可能乱序。例如 INCR 计数器在不同节点的执行顺序可能不一致。

解决方案:对顺序敏感的操作,在双写期间强制走源端,切换完成后再放开。

5.3 影子流量验证

影子流量(Shadow Traffic)是指将生产请求同时镜像到目标环境,但不返回目标端的结果给客户端,仅用于观察和对比。这是验证迁移正确性的最佳手段。

func (d *DualWriter) Get(ctx context.Context, key string) (string, error) {
	// 主流程:从源端读取
	val, err := d.Source.Get(ctx, key).Result()
	if err != nil {
		return "", err
	}

	// 影子流程:从目标端读取并比对(异步,不影响响应)
	go func() {
		shadowVal, shadowErr := d.Target.Get(ctx, key).Result()
		if shadowErr != nil {
			metrics.Inc("shadow_read_error")
		} else if shadowVal != val {
			metrics.Inc("shadow_read_mismatch")
			log.Warnf("shadow mismatch: key=%s source=%s target=%s", key, val, shadowVal)
		}
	}()

	return val, nil
}

影子流量验证期通常持续 24-72 小时,期间需要监控比对差异率。若差异率为 0%,说明目标端完全可用,可以安全切换。

六、数据一致性验证方法

6.1 整体校验和对比

迁移完成后,最核心的步骤是验证数据一致性。校验和(Checksum)是最常用的方法,但 Redis 并不直接提供数据库级别的校验和。

基于 Key 的抽样校验

#!/bin/bash
# checksum_compare.sh:对比源端和目标端指定 Key 的 SHA256

SOURCE_HOST=$1
TARGET_HOST=$2
KEY=$3

source_val=$(redis-cli -h $SOURCE_HOST GET "$KEY")
target_val=$(redis-cli -h $TARGET_HOST GET "$KEY")

source_hash=$(echo -n "$source_val" | sha256sum | awk '{print $1}')
target_hash=$(echo -n "$target_val" | sha256sum | awk '{print $1}')

if [ "$source_hash" == "$target_hash" ]; then
  echo "MATCH: $KEY"
else
  echo "MISMATCH: $KEY (source=$source_hash target=$target_hash)"
fi

6.2 批量抽样校验

对于百万级 Key 的集群,逐条比对不现实。应采用分层抽样策略:

#!/bin/bash
# 从每个槽位随机抽样若干 Key 进行比对

for slot in $(seq 0 16383); do
  # 随机获取该槽位的一个 Key
  key=$(redis-cli -h source CLUSTER GETKEYSINSLOT $slot 1 | head -1)
  if [ -n "$key" ]; then
    type=$(redis-cli -h source TYPE "$key")
    case $type in
      string)
        s=$(redis-cli -h source GET "$key")
        t=$(redis-cli -h target GET "$key")
        [ "$s" == "$t" ] || echo "MISMATCH string: $key"
        ;;
      hash)
        s=$(redis-cli -h source HGETALL "$key" | sort)
        t=$(redis-cli -h target HGETALL "$key" | sort)
        [ "$s" == "$t" ] || echo "MISMATCH hash: $key"
        ;;
      zset)
        s=$(redis-cli -h source ZRANGE "$key" 0 -1 WITHSCORES)
        t=$(redis-cli -h target ZRANGE "$key" 0 -1 WITHSCORES)
        [ "$s" == "$t" ] || echo "MISMATCH zset: $key"
        ;;
    esac
  fi
done

6.3 RDB 级别对比工具

对于全量迁移的终极验证,可以对比源端和目标端生成的 RDB 文件:

# 1. 在源端和目标端同时执行 BGSAVE
redis-cli -h source BGSAVE
redis-cli -h target BGSAVE

# 2. 等待 save 完成

# 3. 使用 rdb-tools 对比 Key 数量和总大小
rdb -c memory /var/lib/redis/source/dump.rdb | wc -l
rdb -c memory /var/lib/redis/target/dump.rdb | wc -l

# 4. 对比具体 Key 的大小
rdb -c memory /var/lib/redis/source/dump.rdb > source.csv
rdb -c memory /var/lib/redis/target/dump.rdb > target.csv
diff source.csv target.csv

6.4 增量校验:redis-full-check

阿里云提供了 redis-full-check 工具,专门用于对比源和目标 Redis 的数据一致性,支持全量对比和增量对比两种模式。

# 全量对比
./redis-full-check -s "192.168.1.10:6379;source_password" \
  -t "192.168.2.10:6379;target_password" \
  --comparetype=1 --qps=1000

# 参数说明:
# --comparetype=1  全量对比
# --comparetype=2  增量对比(基于 checksum,速度快)
# --qps           限制对比速度,避免影响线上

检查结果分为三类:

  • Keylack:目标端缺少某个 Key。
  • ValueNotEqual:两端 Key 的值不一致。
  • TypeNotEqual:两端 Key 的类型不一致。

对于少量不一致的 Key,需要排查是否在对比窗口期内发生了写入(即还未同步到目标端),或是双写逻辑存在 Bug。

七、零停机迁移完整操作流程

7.1 迁移前准备清单

一个严谨的零停机迁移需要完成以下准备工作:

  1. 容量评估:目标环境内存 >= 源端数据量 x 1.5(预留缓冲和 BigKey 膨胀)。
  2. 网络测试:源端与目标端之间的网络延迟和带宽。跨机房时建议使用 iperf3 测试。
  3. BigKey 扫描:使用 --bigkeys 和 rdb-tools 全面扫描,提前治理。
  4. 客户端兼容性:确认客户端支持 Cluster 协议(MOVED/ASK 重定向)。
  5. 监控告警:在目标端预先部署监控,包括内存、QPS、延迟、连接数。
  6. 回滚方案:确保可以在切换后快速回退到源端。
# 网络带宽测试
iperf3 -c target_host -t 60 -i 5

# 预估全量同步时间(按 100MB/s 计算)
# 数据量 50GB / 100MB/s = 500 秒 ≈ 8.3 分钟
redis-cli -h source INFO memory | grep used_memory_human

7.2 分阶段切换流程

阶段一:影子验证(1-3天)
   -> 启动 redis-shake 全量同步
   -> 进入实时增量同步
   -> 开启影子流量,对比读写结果
   -> 目标端压测确认性能满足要求

阶段二:双写预热(1-3天)
   -> 停止影子流量
   -> 开启双写(先异步,后同步)
   -> 监控双写延迟和差异率
   -> 全量一致性校验通过

阶段三:流量切换
   -> 将源端设为只读(stop-writes-on-bgsave-error yes 或 config set)
   -> 确认增量同步延迟为 0
   -> 停止 redis-shake
   -> 修改客户端配置,流量切到目标端
   -> 观察业务量健康度

阶段四:观察期(3-7天)
   -> 保持源端运行,随时可回滚
   -> 确认目标端稳定
   -> 下线源端并归档

7.3 回滚方案

迁移过程中发生严重问题时必须能快速回滚。回滚的核心是确保源端在整个切换前后都保持一致状态。

# 回滚操作(假设目标端已写入新数据且需要回滚)
# 1. 将目标端最新的增量数据同步回源端
# 2. 切换客户端回到源端
# 3. 源端恢复写入

# 源端设为只读(切换前设置,便于回滚)
redis-cli -h source CONFIG SET slave-read-only no
# 注意:主节点没有 slave-read-only 配置,如果源端是单节点
# 可以在应用层通过开关控制写操作路由

对于使用双写模式的情况,回滚最简方案是修改客户端路由配置,将写操作指回源端。由于双写期间源端数据也是最新的,业务可以立即恢复。

八、跨区域复制与异地多活

8.1 跨机房复制方案

跨境/跨区域的 Redis 部署面临的核心矛盾是延迟。Redis 的复制是异步的,跨机房复制延迟通常在 10-100ms 之间,因此原生主从复制只适合灾备场景,不适合双活写入。

方案一:主从跨机房复制(冷备)

北京机房(主)          上海机房(从)
  Redis Master  ----->  Redis Slave
  写入 + 读取             只读 / 灾备

配置要点:开启 repl-diskless-sync yes,使用无盘复制避免磁盘 I/O 瓶颈。跨公网时建议开启 TLS:tls-replication yes

方案二:双向复制(CRDT)

开源社区提供了基于 CRDT(Conflict-free Replicated Data Types)的双向复制方案,如 Redis Enterprise 的 Active-Active 或阿里的 Tair。CRDT 允许双机房同时写入,自动解决冲突。

  北京机房                  上海机房
  Redis A <=============> Redis B
  (CRDT 协议)              (CRDT 协议)

CRDT 的局限是并非所有 Redis 数据类型都能天然支持冲突消解。String 的 SET 操作在冲突时无法自动合并,需要业务层定义冲突解决策略(如 Last-Write-Wins 或向量时钟)。

8.2 全球负载均衡与就近读取

对于跨国业务,可以在多个区域部署 Redis 从节点,配合 DNS 或 CDN 将用户请求路由到就近节点读取。写入则统一路由到主库所在区域。

       用户请求
          |
    +-----+-----+
    |           |
  美国用户   中国用户
    |           |
  DNS 路由    DNS 路由
    |           |
 US-Redis    CN-Redis
   Slave      Slave
    |           |
    +-----+-----+
          |
     Master Redis
       (主写)

此方案的 RPO(恢复点目标)取决于复制延迟,RTO(恢复时间目标)取决于 DNS TTL 和客户端重连速度。对于数据一致性要求极高的金融交易场景,RPO 趋近于 0 意味着不能使用异步复制,必须引入同步确认机制,但这会大幅牺牲性能。

8.3 跨云迁移

跨云迁移(例如从阿里云迁到 AWS ElastiCache 或自建 IDC)的核心挑战是网络连通性和云厂商的差异化特性。

公网 Tunnel + redis-shake

# 通过 SSH Tunnel 打通跨云网络
ssh -fN -L 6380:source-redis.internal:6379 bastion.host.com

# redis-shake 配置指向本地 Tunnel 端口
[sync_reader]
address = "127.0.0.1:6380"
password = "source_password"

注意事项

  • 公网传输必须启用 TLS,防止数据泄露。
  • 不同云厂商的 Redis 服务可能有协议差异(如 AWS ElastiCache 禁用了某些管理命令),迁移前需要逐一确认。
  • Cluster 配置端点(Configuration Endpoint)在不同云中的实现方式不同,切换时需要客户端适配。

九、Redis on Kubernetes 部署与迁移

9.1 StatefulSet 部署方案

在 Kubernetes 上部署 Redis,StatefulSet 是管理有状态服务的标准选择。每个 Pod 拥有稳定的网络标识和独立的持久存储。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-cluster
spec:
  serviceName: redis-cluster
  replicas: 6
  selector:
    matchLabels:
      app: redis-cluster
  template:
    metadata:
      labels:
        app: redis-cluster
    spec:
      containers:
      - name: redis
        image: redis:7.2-alpine
        ports:
        - containerPort: 6379
          name: redis
        - containerPort: 16379
          name: cluster
        command:
        - redis-server
        - /etc/redis/redis.conf
        volumeMounts:
        - name: data
          mountPath: /data
        - name: config
          mountPath: /etc/redis
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi

9.2 Headless Service 与节点发现

Cluster 模式要求每个节点都有固定的、可被其他节点访问的地址。在 K8s 中通过 Headless Service 实现:

apiVersion: v1
kind: Service
metadata:
  name: redis-cluster
spec:
  clusterIP: None  # Headless Service
  selector:
    app: redis-cluster
  ports:
  - port: 6379
    name: redis
  - port: 16379
    name: cluster

创建 Headless Service 后,每个 Pod 的 DNS 解析形如 redis-cluster-0.redis-cluster.default.svc.cluster.local,其他节点通过这个地址相互发现。

9.3 初始化集群

部署完 6 个 Pod(3 主 3 从)后,需要在一个临时 Pod 中执行 redis-cli --cluster create

kubectl run redis-init --rm -i --restart=Never \
  --image=redis:7.2-alpine -- \
  redis-cli --cluster create \
  redis-cluster-0.redis-cluster:6379 \
  redis-cluster-1.redis-cluster:6379 \
  redis-cluster-2.redis-cluster:6379 \
  redis-cluster-3.redis-cluster:6379 \
  redis-cluster-4.redis-cluster:6379 \
  redis-cluster-5.redis-cluster:6379 \
  --cluster-replicas 1 --cluster-yes

9.4 Redis Operator 方案

手动管理 StatefulSet + Headless Service 足以运行 Redis,但扩缩容、故障恢复仍需要人工介入。Operator 模式将运维知识编码为控制器,实现 Redis 集群的自动化运维。

常用 Redis Operator

Operator维护方特性
Redis Operator (Spotahome)社区Cluster 管理、自动故障转移
Redis Enterprise OperatorRedisLabs官方企业级方案
Alibaba Redis Operator阿里云深度集成云特性
KubeDB RedisAppsCode多数据库通用方案

Spotahome Redis Operator 部署示例

apiVersion: databases.spotahome.com/v1
kind: RedisFailover
metadata:
  name: redis-cluster
spec:
  sentinel:
    replicas: 3
  redis:
    replicas: 3
    storage:
      persistentVolumeClaim:
        spec:
          resources:
            requests:
              storage: 10Gi

这个 Operator 自动管理 Sentinel + Redis 主从架构。但原生 Cluster 模式的管理则更加复杂,因为需要处理槽位分配和节点发现的动态性。

9.5 K8s 内外的数据迁移

从外部 IDC 迁入 K8s

  1. 在 K8s 中部署目标 Redis Cluster。
  2. 使用 NodePort 或 LoadBalancer 暴露目标 Redis 的服务端口。
  3. 在外部 IDC 启动 redis-shake,将数据同步到 K8s 暴露的地址。
  4. 切换应用流量到 K8s 内部的 Redis 服务。
# Expose Redis Cluster for external migration
kubectl expose statefulset redis-cluster \
  --type=NodePort --port=6379 --name=redis-migrate-svc

从 K8s 迁出到云托管服务

  1. 对云托管 Redis 开启白名单,允许 K8s 集群出口 IP 访问。
  2. 在 K8s 内部署 redis-shake 的 Job,将数据同步到云 Redis。
  3. 验证一致性后,修改应用的 Redis 连接地址为云托管端点。
apiVersion: batch/v1
kind: Job
metadata:
  name: redis-migrate
spec:
  template:
    spec:
      containers:
      - name: redis-shake
        image: registry.aliyuncs.com/aliyun_kube/redis-shake:v3
        command: ["./redis-shake", "sync.toml"]
        volumeMounts:
        - name: config
          mountPath: /app/sync.toml
          subPath: sync.toml
      restartPolicy: Never

9.6 数据持久化与备份在 K8s 中的注意事项

K8s 中 Redis 的持久化面临两个特有挑战:

  1. Pod 漂移与存储绑定:使用 StatefulSet + PVC 可以确保 Pod 重建后绑回同一个 PV,但节点故障导致 Pod 漂移到其他节点时,依赖 CSI 驱动的跨节点挂载能力。

  2. RDB/AOF 与容器存储层:容器存储通常比本地 SSD 性能差。对于写入密集型场景,建议在 K8s 中使用本地存储(Local PV)而非网络存储(NFS/CephFS)。Redis 7.0 的多部分 AOF 在慢存储上能显著降低 fsync 的阻塞时间。

# 使用 Local PV(高性能本地 SSD)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-ssd
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

十、总结

Redis 数据迁移从来不是简单的 “dump & restore”,而是涉及工具选型、网络架构、数据校验、业务改造的系统性工程。本文梳理的九大主题形成了完整的迁移方法论:

  1. 场景判断决定工具选择:小规模用 MIGRATE,大规模用 redis-shake,长期同步用 CDC。
  2. Cluster 槽位迁移是扩容缩容的核心,掌握 reshardrebalance 命令即可应对大多数集群调整需求。
  3. BigKey 治理是迁移成功的先决条件,迁移前必须扫描和拆分,否则槽迁移或全量同步都会陷入困境。
  4. 双写 + 影子流量是零停机切换的黄金组合,前者保证两边数据一致,后者验证目标端正确性。
  5. 一致性校验不能仅凭抽样,应组合使用 Key 级比对、RDB 工具分析和 redis-full-check 全量扫描。
  6. 跨区域复制需要容忍延迟,RPO 和 RTO 的平衡要从业务容忍度出发设计。
  7. K8s 部署通过 StatefulSet + Headless Service 解决有状态问题,Operator 则进一步减轻了运维负担。

无论采用哪种方案,生产环境迁移都遵循一条铁律:先在准生产环境完整演练一遍。数据迁移无法回退,每一次操作都需要可观察、可验证、可回滚。祝你的 Redis 迁移一切顺利。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. 缓存架构演进之路:从单机 Redis 到亿级分布式多级缓存体系
  2. Redis 7.x 重大新特性与架构升级深度解析
  3. Redis 消息队列深度对比:Pub/Sub、Streams 与 Kafka/RabbitMQ 选型指南