不能直接用scan+get做增量迁移,因scan不保证顺序、无可靠断点续传能力,集群下易漏key或重复;生产推荐解析aof日志实现有序、可定位偏移的增量同步。

为什么不能直接用 SCAN + GET 做增量迁移
因为 Redis 的 SCAN 不保证顺序,也不提供“从某次游标继续”的可靠断点续传能力——如果迁移中途失败,你无法精确知道最后成功处理的 key 是哪一个。更麻烦的是,SCAN 在集群模式下行为不一致(某些客户端会自动打散命令到 slot,但 SCAN 本身不支持跨节点游标),容易漏 key 或重复拉取。
实际生产中,更稳妥的方式是依赖 Redis 自身的变更日志机制:
-
Redis 6.2+支持ACL LOG和MONITOR,但性能开销大、不可靠,仅适合调试 -
Redis 7.0+正式引入Redis Streams配合KEYSPACE notifications可构建轻量级变更捕获,但需业务主动写入 stream - 最通用、零侵入的做法:启用
AOF并解析其内容——AOF 是追加写、有序、可定位偏移的纯文本日志,天然适合增量同步
用 redis-aof-parser 解析 AOF 实现增量位点控制
Go 生态里没有官方 AOF 解析器,但社区有轻量可用的 github.com/mediocregopher/redis-aof-parser(注意不是同名废弃项目)。它能把 AOF 文件按行解析为 redis.Command 结构体,包含 Cmd、Args、Offset(字节偏移)等字段。
关键实操点:
- 迁移前先做一次全量 dump(
SAVE或BGSAVE),记录此时INFO persistence中的aof_current_size和aof_base_size - 启动增量监听时,打开 AOF 文件,用
parser.ParseFile从上次保存的Offset开始读(不是行号!是字节偏移) - 对每个命令,过滤掉
SELECT、EXPIRE、DEL等非数据写入操作;只处理SET、HSET、LPUSH等真正修改 value 的命令 - 遇到
CONFIG SET appendonly yes或bgrewriteaof触发的 AOF 重写,需监听INFO persistence的aof_rewrite_in_progress,暂停解析并切换到新 AOF 文件
如何避免目标 Redis 被压垮或写乱序
源 Redis 的 AOF 是单线程顺序写,但你的 Go 迁移程序是并发消费——如果直接起多个 goroutine 往目标 Redis 写,会破坏命令时序,导致最终状态不一致(比如先写 INCR 后写 SET,结果 counter 被覆盖)。
必须保序写入,同时兼顾吞吐:
- 用一个 goroutine 顺序读 AOF 并分发命令到 channel,channel 的 buffer 大小设为 1024,避免阻塞解析
- 下游用固定数量(如 4 个)worker 从 channel 拉取命令,但每个 key 的所有操作必须路由到同一个 worker(用
hash(key) % N),确保单 key 内部有序 - 对
EXPIREAT、PEXPIREAT等带时间戳的命令,需将源 Redis 的unix timestamp转为目标 Redis 当前时间偏移,否则过期逻辑错乱 - 禁用目标 Redis 的
appendonly,避免二次写 AOF 拖慢速度;迁移完成后再开启并执行BGREWRITEAOF
迁移中断后如何精准恢复
别存“已处理多少条”,要存“已处理到 AOF 文件的第 N 字节”。因为 AOF 里一条命令可能跨多行(如 MSET 多 key),行数不可靠。
每次成功写入目标 Redis 后,立刻把当前命令的 Offset + 该命令总长度(含换行符)写入本地文件(如 checkpoint.offset),用 os.WriteFile 原子覆盖。
重启时:
- 检查源 AOF 文件大小是否增长,若没变说明无新写入,直接退出
- 若增长,用
os.Stat获取当前 AOF 大小,与 checkpoint 中的 offset 比较;若 offset > 当前大小,说明 AOF 被重写过,需回退到上一个全量备份点重新开始 - 解析时跳过开头部分,用
io.Seeker定位到 checkpoint offset,再调用parser.ParseReader
真正的难点不在代码量,而在于 AOF 重写、主从切换、RDB/AOF 混合持久化这些边界场景的检测和响应——它们不会报错,但会导致数据静默丢失。











