Redis主从同步以及Redis-Shake数据同步实战

背景

项目在进行上云,原先redis是自建的,现需要对自建的redis中的数据迁移到云上的redis中,遂调研当前有哪些工具支持redis的全量部署数据的迁移和增量数据迁移。经过调研当前使用比较多的有 Redis-Shake 以及云厂商提供的商用的同步工具,我们公司当前采购的是阿里云的redis,所以阿里云也提供了DTS的工具,由于阿里云的DTS在增量同步数据这块是按时间收费的,而且 Redis-Shake 当前是能正常满足我们的迁移需求的,所以下面主要介绍Redis-Shake 这个工具的使用以及 Redis 的主从复制的原理等

Redis主从复制以及Redis-Shake架构

和 Mysql 领域的 Canal 的原理类似,Redis-Shake 也是通过伪装成 Redis Master节点的 Slave 节点来获取主节点数据,之后进行主动推送给 需要同步的目标机器Redis, 所以先来了解下 Redis 主从同步的过程,然后再来了解下Redis-Shake 的工作过程。

Redis 主从(Master-Replica)数据同步过程

Redis 的主从同步分为两种主要模式:全量同步(Full Synchronization)和部分同步(Partial Synchronization)。

阶段一:建立连接与协商

当一个副本节点(Replica)要从一个主节点(Master)同步数据时,整个过程开始。

  1. 设置主节点:副本节点通过执行 REPLICAOF <master_ip> <master_port> 命令(老版本是 SLAVEOF)来指定其主节点。
  2. 建立网络连接:副本与主节点之间建立一个 TCP socket 连接。
  3. 发送 PING:副本向主节点发送 PING 命令,如果收到 PONG 响应,说明网络通信正常。
  4. 身份验证:如果主节点设置了 requirepass,副本需要发送 AUTH 进行验证。
  5. 端口和能力协商:副本通过 REPLCONF listening-port 告知主节点自己的监听端口,并通过 REPLCONF capa eof capa psync2 等告知主节点自己支持的能力(例如,支持无盘复制、支持 PSYNC2 协议等)。
  6. 发起同步请求:副本向主节点发送 PSYNC 命令,请求开始同步数据。这是最关键的一步。
阶段二:同步数据(全量或部分)

主节点收到 PSYNC 命令后,会根据副本提供的 runid 和 offset 决定采用哪种同步模式。

场景 A:全量同步 (Full Synchronization)

在以下情况下会触发全量同步:

  • 副本是第一次连接主节点。
  • 副本的 runid 与主节点的 runid 不匹配(说明主节点重启过或发生了故障转移)。
  • 副本请求的 offset 在主节点的复制积压缓冲区(Replication Backlog)中已经不存在(即副本断线时间太长)。

全量同步的详细步骤:

  1. 主节点响应:主节点回复 +FULLRESYNC ,告知副本需要进行全量同步,并提供自己当前的 runid 和 offset。
  2. 生成 RDB:主节点执行 BGSAVE 命令,在后台异步生成一个 RDB 快照文件。
  3. 缓冲写命令:在 BGSAVE 期间,主节点会将所有新的写命令(如 SET, INCR 等)缓存到一个内存缓冲区中。
  4. 发送 RDB:RDB 文件生成后,主节点将其内容发送给副本。
  5. 副本接收 RDB:副本接收到 RDB 文件后,会清空自己当前的所有数据,然后加载 RDB文件,将自己的数据库状态恢复到主节点执行 BGSAVE 时的状态。
  6. 发送缓冲命令:主节点将之前在内存缓冲区中缓存的写命令,逐一发送给副本。
  7. 副本执行命令:副本执行从主节点发来的所有写命令,使其数据库状态与主节点完全一致。至此,全量同步完成,主从状态达到一致。

场景 B:部分同步 (Partial Synchronization)

如果副本不是第一次连接,并且它请求的 runid 与主节点匹配,且 offset仍在主节点的复制积压缓冲区内,则会触发部分同步。

  1. 主节点响应:主节点回复 +CONTINUE,表示可以进行部分同步。
  2. 发送缺失命令:主节点从复制积压缓冲区中,将从副本 offset 之后的所有命令发送给副本。
  3. 副本执行命令:副本接收并执行这些命令。部分同步非常快,因为它只传输断线期间的少量增量数据,避免了生成和传输庞大 RDB 文件的开销。
阶段三:命令传播 (Command Propagation)

一旦主从状态同步完成,它们就进入了命令传播阶段

  • 主节点每执行一个写命令,都会异步地将该命令发送给所有连接的副本。
  • 副本接收并执行这些命令,从而实时保持与主节点的数据同步。
  • 为了维持连接,主从之间会周期性地发送心跳命令(主节点发 PING,副本回 REPLCONF ACK )
涉及的核心命令详解
  • REPLICAOF <master_ip> <master_port> (或 SLAVEOF)

    • 作用:由客户端或副本自己执行,用于设置要复制的主节点。执行 REPLICAOF NO ONE 可以使副本节点变回主节点。这是触发整个复制流程的"用户命令"。
  • PSYNC

    • 作用:由副本在内部自动发送给主节点,是复制协议的核心命令。

    • 参数:

      • runid: 副本所记录的主节点的运行 ID。如果是第一次连接,runid 为 ?。
      • offset: 副本已接收到的数据偏移量。如果是第一次连接,offset 为 -1。
    • 主节点响应:

      • +FULLRESYNC : 强制进行全量同步。
      • +CONTINUE: 同意进行部分同步。
      • -ERR ...: 拒绝同步(例如,runid 不匹配且无法进行部分同步)
  • REPLCONF

    • 作用:一个用于配置和确认的内部命令。

    • 常见子命令:

      • REPLCONF listening-port : 副本告知主节点自己的监听端口。
      • REPLCONF ip-address : 副本告知主节点自己的 IP 地址。
      • REPLCONF capa : 副本告知主节点自己支持的能力(如 eof, psync2)。
      • REPLCONF ACK :非常重要,副本周期性地向主节点报告自己已处理的数据偏移量。这既是心跳,也让主节点知道复制的健康状况,并据此判断复制积压缓冲区中的数据是否可以被覆盖。
Redis-Shake 伪装成副本的原理和过程

Redis-shake 的核心原理就是完全遵守并实现了上述的 Redis 复制协议,从主节点的视角来看,Redis-shake

就是一个行为标准的 Redis 副本。

  1. 配置与启动:您在 redis-shake.toml 中配置源 Redis(即 Master)的地址、端口和密码。

  2. 模拟连接握手:

    1. Redis-shake 启动后,会像一个真正的副本一样,与源 Redis 建立 TCP 连接。
    2. 它会发送 PING、AUTH、REPLCONF listening-port 等一系列命令,完成连接的初始化和身份验证。这个过程与真实副本完全一致。
  3. 发起同步请求:

    1. 握手成功后,Redis-shake 向源 Redis 发送 PSYNC ? -1,请求进行数据同步。
  4. 处理数据流(核心区别):

    1. 源 Redis 收到请求后,开始进行全量同步,并向 Redis-shake 发送 RDB 文件流。

    2. 关键区别在于:

      • 一个真实的 Redis 副本会接收 RDB 数据,并将其加载到自己的内存中。
      • Redis-shake 则在接收 RDB数据流时,实时地解析(Parse)这个数据流。它并不把数据加载到自己的内存里,而是根据解析出的键值对(Key-Value),将其转换成目标端需要的格式。
      • 例如,如果目标端是另一个 Redis,它就会为每个键值对生成对应的 RESTORE 命令或 SET、HSET等命令,然后发送给目标 Redis。如果目标端是文件,它就会将键值对格式化后写入文件。
  5. 处理增量命令:

    1. 当 RDB 同步完成后,源 Redis 会进入命令传播阶段,开始向 Redis-shake 发送增量写命令。
    2. Redis-shake 接收到这些增量命令后,同样不会在自己身上执行,而是将这些命令转换并应用到目标端。
  6. 维持心跳:

    1. 在整个同步过程中,Redis-shake 会像一个好学生一样,周期性地向源 Redis 发送 REPLCONF ACK ,报告自己的同步进度,防止连接因超时而中断。

使用方式

Redis-Shake 非常容易上手,而且提供了相当强大的功能,包括自动的全量数据同步以及增量数据同步,相关文档见下面的参考

下载Redis-Shake工具到Linux服务器

保证 Redis-Shake 所在机器能访问 自建Redis机器 以及 阿里云redis代理地址

bash 复制代码
## 下载地址
https://github.com/tair-opensource/RedisShake/releases
  1. 配置redis-shake

shake.toml 配置文件修改,主要修改sync_reader 以及 redis_writer的配置

ini 复制代码
[sync_reader]
cluster = false            # Set to true if the source is a Redis cluster
address = "127.0.0.1:6379" # For clusters, specify the address of any cluster node; use the master or slave address in master-slave mode
username = ""              # Keep empty if ACL is not in use
password = "local@test"              # Keep empty if no authentication is required
tls = false                # Set to true to enable TLS if needed
sync_rdb = true            # Set to false if RDB synchronization is not required
sync_aof = true            # Set to false if AOF synchronization is not required
prefer_replica = false     # Set to true to sync from a replica node
try_diskless = false       # Set to true for diskless sync if the source has repl-diskless-sync=yes

#[scan_reader]
#cluster = false            # set to true if source is a redis cluster
#address = "127.0.0.1:6379" # when cluster is true, set address to one of the cluster node
#username = ""              # keep empty if not using ACL
#password = ""              # keep empty if no authentication is required
#tls = false
#dbs = []                   # set you want to scan dbs such as [1,5,7], if you don't want to scan all
#scan = true                # set to false if you don't want to scan keys
#ksn = false                # set to true to enabled Redis keyspace notifications (KSN) subscription
#count = 1                  # number of keys to scan per iteration

# [rdb_reader]
# filepath = "/tmp/dump.rdb"

# [aof_reader]
# filepath = "/tmp/.aof"
# timestamp = 0            # subsecond

[redis_writer]
cluster = false            # set to true if target is a redis cluster
address = "云上地址:6379" # when cluster is true, set address to one of the cluster node
username = ""              # keep empty if not using ACL
password = "test"              # keep empty if no authentication is required
tls = false
off_reply = false          # turn off the server reply

# [file_writer]
# filepath = "/tmp/cmd.txt"
# type = "cmd" #cmd,aof,json (default cmd)

[filter]
# Allow keys with specific prefixes or suffixes
# Examples:
#   allow_keys = ["user:1001", "product:2001"]
#   allow_key_prefix = ["user:", "product:"]
#   allow_key_suffix = [":active", ":valid"]
#   allow A collection of keys containing 11-digit mobile phone numbers
#   allow_key_regex = [":\d{11}:"]
# Leave empty to allow all keys
allow_keys = []
allow_key_prefix = []
allow_key_suffix = []
allow_key_regex = []

# Block keys with specific prefixes or suffixes
# Examples:
#   block_keys = ["temp:1001", "cache:2001"]
#   block_key_prefix = ["temp:", "cache:"]
#   block_key_suffix = [":tmp", ":old"]
#   block test 11-digit mobile phone numbers keys
#   block_key_regex = [":test:\d{11}:"]
# Leave empty to block nothing
block_keys = []
block_key_prefix = []
block_key_suffix = []
block_key_regex = []

# Specify allowed and blocked database numbers (e.g., allow_db = [0, 1, 2], block_db = [3, 4, 5])
# Leave empty to allow all databases
allow_db = []
block_db = []

# Allow or block specific commands
# Examples:
#   allow_command = ["GET", "SET"]  # Only allow GET and SET commands
#   block_command = ["DEL", "FLUSHDB"]  # Block DEL and FLUSHDB commands
# Leave empty to allow all commands
allow_command = []
block_command = []

# Allow or block specific command groups
# Available groups:
#   SERVER, STRING, CLUSTER, CONNECTION, BITMAP, LIST, SORTED_SET,
#   GENERIC, TRANSACTIONS, SCRIPTING, TAIRHASH, TAIRSTRING, TAIRZSET,
#   GEO, HASH, HYPERLOGLOG, PUBSUB, SET, SENTINEL, STREAM
# Examples:
#   allow_command_group = ["STRING", "HASH"]  # Only allow STRING and HASH commands
#   block_command_group = ["SCRIPTING", "PUBSUB"]  # Block SCRIPTING and PUBSUB commands
# Leave empty to allow all command groups
allow_command_group = []
block_command_group = []

# Function for custom data processing
# For best practices and examples, visit:
# https://tair-opensource.github.io/RedisShake/zh/filter/function.html
function = ""

[advanced]
dir = "data"
ncpu = 0        # runtime.GOMAXPROCS, 0 means use runtime.NumCPU() cpu cores
pprof_port = 0  # pprof port, 0 means disable
status_port = 0 # status port, 0 means disable

# log
log_file = "shake.log"
log_level = "info"     # debug, info or warn
log_interval = 5       # in seconds
log_rotation = true    # enable log rotation
log_max_size = 512     # MiB, logs max size to rotate, default 512 MiB
log_max_age = 7        # days, logs are kept, default 7 days
log_max_backups = 3    # number of log backups, default 3
log_compress = true    # enable log compression after rotate, default true

# redis-shake gets key and value from rdb file, and uses RESTORE command to
# create the key in target redis. Redis RESTORE will return a "Target key name
# is busy" error when key already exists. You can use this configuration item
# to change the default behavior of restore:
# panic:   redis-shake will stop when meet "Target key name is busy" error.
# rewrite: redis-shake will replace the key with new value.
# skip:  redis-shake will skip restore the key when meet "Target key name is busy" error.
rdb_restore_command_behavior = "panic" # panic, rewrite or skip

# redis-shake uses pipeline to improve sending performance.
# Adjust this value based on the destination Redis performance:
# - Higher values may improve performance for capable destinations.
# - Lower values are recommended for destinations with poor performance.
# 1024 is a good default value for most cases.
pipeline_count_limit = 1024

# This setting corresponds to the 'client-query-buffer-limit' in Redis configuration.
# The default value is typically 1GB.
# It's recommended not to modify this value unless absolutely necessary.
target_redis_client_max_querybuf_len = 1073741824  # 1GB in bytes

# This setting corresponds to the 'proto-max-bulk-len' in Redis configuration.
# It defines the maximum size of a single string element in the Redis protocol.
# The value must be 1MB or greater. Default is 512MB.
# It's recommended not to modify this value unless absolutely necessary.
target_redis_proto_max_bulk_len = 512_000_000

# If the source is Elasticache, you can set this item. AWS ElastiCache has custom
# psync command, which can be obtained through a ticket.
aws_psync = "" # example: aws_psync = "10.0.0.1:6379@nmfu2sl5osync,10.0.0.1:6379@xhma21xfkssync"

# destination will delete itself entire database before fetching files
# from source during full synchronization.
# This option is similar redis replicas RDB diskless load option:
#   repl-diskless-load on-empty-db
empty_db_before_sync = false

[module]
# The data format for BF.LOADCHUNK is not compatible in different versions. v2.6.3 <=> 20603
target_mbbloom_version = 20603
  1. 启动redis-shake
bash 复制代码
nohup redis-shake ./shake.toml &
  1. 全量数据同步验证

进程启动之后,会自动进行全量数据同步,如上面的原理分析一样,redis-shake会请求源redis进行数据同步,这次因为相当于是新增的redis副本,所以是一次全量的数据同步,源redis会后台进行bgsave生成rdb文件,之后传输给redis-shake, 同时源redis的增量数据会被源redis保存缓存,等待全量数据同步之后,自动会进行增量数据同步

增量数据同步验证

使用 redis-benchmark 模拟增量数据过程,往测试的162的redis 发送1000w的redis的写,

bash 复制代码
taskset -c 0-7 /usr/local/redis/bin/redis-benchmark -r 10000000 -n 10000000 -t set -h 127.0.0.1 -a 'local@test'  -c 8 -d 8 -P 2 

本地的redis 实际生成 6320024

云上redis通过 redis-shake的增量同步也有了 6320024

总结

本文通过一个 Redis 的数据迁移的需求,深入了解了redis的主从同步的过程以及原理,之后调研Redis的数据同步工具,包括 Redis-Shake 和 云厂商提供的同步工具,最后经过动手搭建一个本地的redis ,然后通过redis-shake 工具成功进行数据的全量和增量同步。最后感谢下开源贡献者吧,然后作为开发者的一员,咱们也要多多参与开源

参考

1、Use Redis-shake To Migrate Self-built Redis To Alibaba Cloud

2、tair-opensource.github.io/RedisShake/...

相关推荐
阿里云云原生3 小时前
实战|魔方文娱全链路可观测架构设计:SLS+ARMS+Prometheus+云拨测落地指南
云原生
和光同尘20234 小时前
使用Rancher快速部署K8S集群
docker·云原生·容器·kubernetes·centos·rancher·虚拟机
Fency咖啡5 小时前
redis进阶 - 底层数据结构
数据结构·数据库·redis
知其然亦知其所以然5 小时前
面试官笑了:我用这套方案搞定了“2000w vs 20w”的Redis难题!
redis·后端·面试
The Sheep 20235 小时前
MicroService(Redis)
数据库·redis·c#
hour_go7 小时前
DeepHunt微服务故障定位系统核心技术解析1
微服务·云原生·架构
无心水7 小时前
【中间件:Redis】1、Redis面试核心:线程模型深度解析(6.0前后变化+工作流程)
数据库·redis·面试·redis面试·redis原理·redis线程模型·后端技术
程序员古德7 小时前
25年11月软考架构真题《论无服务器架构(Serverless)》考后复盘总结
云原生·架构·serverless