Redis 的持久化策略选择,直接决定了生产环境故障恢复时数据丢失的上限。很多团队将 RDB 和 AOF 视为二选一的方案,或者在主节点只开 RDB、从节点才开 AOF,这种割裂的配置往往在真出问题时才发现恢复出来的数据要么缺斤少两,要么恢复速度慢到影响业务 SLA。混合持久化模式正是为了解决这个矛盾而生的,但开启混合持久化后的备份策略,和单纯 RDB 或单纯 AOF 完全不同,搞错备份对象或备份时机,一样会踩坑。

混合持久化的文件结构决定了你不能只备份 RDB 文件

混合持久化开启后,Redis 生成的 AOF 文件在物理结构上发生了根本变化。文件前半段不再是文本协议的写命令,而是一个完整 RDB 格式的二进制快照,紧接着才是增量 AOF 日志。这个文件通常命名为 appendonly.aof,但它本质上是一个“RDB 头部 + AOF 尾部”的混合体。如果你仍然按照旧习惯只去备份 dump.rdb,那么混合模式下这个文件可能根本不存在,或者存在也是过期的——因为混合持久化配置下,Redis 的主持久化产出物就是这个混合 AOF 文件,RDB 文件仅在手动 BGSAVE 或某些特定触发条件下才会单独生成。

这意味着你的备份脚本必须调整目标文件。正确的做法是直接备份 appendonly.aof 文件,而不是 dump.rdb。很多运维在迁移或恢复时,从备份存储中拉取了 dump.rdb 覆盖到新节点,结果启动后发现数据是几天前的,这就是没搞清楚混合模式下数据实际写到了哪个文件里。

备份时机:必须在重写完成后抓取,而不是任意时刻拷贝

混合持久化模式下的 AOF 重写过程,是 Redis 创建一个子进程,先将当前内存数据以 RDB 格式写入一个临时文件,再把重写期间累积的增量命令以 AOF 格式追加到临时文件尾部,最后用 rename 系统调用原子地替换旧 AOF 文件。这个过程的关键点在于:重写期间,旧 AOF 文件仍然在接收写入,而新文件正在构建中。如果你在重写过程中直接拷贝 appendonly.aof,拷贝到的是一个正在被替换的文件,要么读到一半文件被切走导致不完整,要么拷贝到的是旧文件而重写刚好完成,新旧文件交替瞬间你拿到了一个即将被删除的版本。

安全的做法是,在备份脚本中先执行一次 BGREWRITEAOF,然后轮询 INFO PERSISTENCE 中的 aof_rewrite_in_progress 字段,确认重写完成且 aof_last_bgrewrite_status 为 ok 之后,再去拷贝 appendonly.aof。这样你拿到的文件一定是重写后最新的完整混合文件,恢复时既能享受 RDB 的快速加载,又有 AOF 尾部保证增量不丢。如果不想主动触发重写,至少要在备份前检查当前是否有重写正在进行,有就等待或跳过本次备份,避免拷贝到中间态文件。

备份文件完整性校验不能只靠文件大小

很多备份系统的校验逻辑是比对文件大小是否大于某个阈值,认为只要不是 0 字节就算备份成功。这在混合持久化场景下远远不够。混合 AOF 文件前半段是 RDB 格式,RDB 头部有固定的魔数 “REDIS” 以及版本号、校验和等字段。一个有效的混合 AOF 文件,其前几个字节必须是 52 45 44 49 53 这个十六进制序列。备份完成后,至少应该用 head 命令读取文件前几十个字节,确认 RDB 头部完整。更进一步,可以用 redis-check-rdb 工具对备份文件进行离线校验,虽然这个工具原本是校验纯 RDB 文件的,但混合文件的 RDB 部分如果损坏,它同样会报错,只是会提示后面有额外数据,这个提示可以忽略。

另外,AOF 尾部的增量日志也需要基本校验。混合 AOF 文件的最后一条有效指令之后,可能会有未完整写入的命令(比如 Redis 崩溃时)。备份前如果 Redis 在正常运行,尾部应该是完整的。你可以在备份后用 redis-check-aof 工具对文件做一次检查,它会扫描 AOF 部分并报告是否有截断。这个步骤花的时间取决于 AOF 尾部大小,通常增量部分不会太大,几秒就能完成。

恢复流程的陷阱:直接启动会触发空数据重写

从备份恢复混合 AOF 文件时,一个极其隐蔽的坑是:如果你把备份的 appendonly.aof 放到新节点的数据目录下,然后直接启动 Redis,Redis 会加载这个混合文件,数据全部恢复。但紧接着,如果 Redis 配置了 AOF 重写触发条件(比如 auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size),而恢复后的数据量相对当前 AOF 文件大小触发了重写阈值,Redis 会立即启动一次 BGREWRITEAOF。问题在于,如果你恢复的备份文件本身已经是重写后的紧凑混合文件,这次额外的重写不仅浪费资源,在极端情况下如果重写过程中再次故障,可能引入不必要的复杂度。

恢复时的最佳实践是:先将 Redis 配置为不自动触发重写(设置 auto-aof-rewrite-percentage 为 0),启动加载完数据后,再恢复原配置。或者启动后手动检查当前 AOF 文件大小与内存数据量的比例,确认是否需要立即重写。这个细节在紧急恢复时很容易被忽略,但确实影响恢复后的稳定性。

主从架构下混合持久化的备份位置选择

在主从复制架构中,很多团队习惯只在从节点做备份,避免影响主节点性能。混合持久化模式下,这个策略需要重新评估。从节点如果开启了混合持久化,它的 AOF 文件内容来源于主节点的复制流。正常情况下,从节点的混合 AOF 文件内容和主节点最终是一致的,但有一个时间窗口问题:从节点执行 AOF 重写时,是从自身内存生成 RDB 头部,而从节点内存数据可能因为复制延迟略落后于主节点。如果你依赖从节点的备份来做灾难恢复,恢复出来的数据可能比主节点少几秒到几十秒的写入。

如果你的业务对数据一致性要求极高,备份应该从主节点抓取,或者在从节点备份前先执行 WAIT 命令确保复制同步完成,但这会引入额外延迟。更务实的做法是,在从节点备份时,记录下备份时刻从节点的复制偏移量(INFO REPLICATION 中的 slave_repl_offset),备份文件配合这个偏移量信息一起存档。恢复时你至少能明确知道数据截止到哪个时间点,便于业务侧判断是否需要从其他渠道补数据。

混合模式下的增量备份策略

全量备份 appendonly.aof 文件是最直接的方式,但如果你的数据量很大,每次全量拷贝网络和存储开销都不小。混合持久化模式天然适合“全量快照 + 增量日志”的备份思路。由于混合 AOF 文件本身就是 RDB 快照加增量 AOF 的组合,你可以在一次全量备份后,后续只备份 AOF 文件的增量部分。具体做法是:记录上一次备份时 AOF 文件的偏移量,下次备份时用 dd 或 tail 命令从该偏移量开始截取新增内容,生成增量备份文件。恢复时先加载全量混合文件,再按顺序追加增量日志。

但这里有个前提:两次备份之间不能发生 AOF 重写。一旦发生重写,AOF 文件会被整体替换,文件 inode 改变,偏移量就失去了参照意义。因此,如果采用增量备份策略,要么关闭自动 AOF 重写,改为在备份窗口内手动触发重写并做全量备份;要么在备份脚本中检测 inode 变化,一旦发现文件被替换,自动回退到全量备份。这个逻辑需要在备份系统中实现,不能靠人工判断。

加密与传输安全

混合 AOF 文件包含完整的内存数据明文,包括所有键值对。备份文件在网络上传输或落盘存储时,必须考虑加密。Redis 本身不提供备份加密功能,这需要在备份脚本层面解决。常用的做法是备份完成后立即用 openssl 或 gpg 对文件进行对称加密,再将密文传输到备份存储。加密操作会消耗 CPU,注意不要在 Redis 重写期间同时进行大文件加密,避免争抢 CPU 导致重写变慢影响主线程响应。可以把加密步骤放在备份传输之后、从本地删除明文之前执行。

监控与告警的关键指标

混合持久化备份的可靠性,最终要靠监控来保障。除了常规的备份成功/失败状态,还需要关注几个 Redis 内部指标:aof_last_bgrewrite_status 表示最近一次重写是否成功,如果这个值为 err,说明 AOF 文件可能已经损坏或磁盘空间不足,此时备份下来的文件也不可靠。aof_current_size 和 aof_base_size 的比值反映了增量部分相对基础快照的大小,如果这个比值持续增长而不触发重写,说明重写条件配置可能不合理,AOF 文件会越来越大,备份和恢复时间都会拉长。

另外,备份系统本身要记录每次备份文件的 MD5 或 SHA256 哈希值,并与 Redis 节点上当前 AOF 文件的哈希比对。如果备份完成后哈希一致,说明文件在传输过程中没有损坏。这些哈希值也应该作为备份元数据存档,方便恢复时做完整性验证。

一个生产可用的备份脚本示例

下面是一个简化但可用的备份脚本逻辑,展示了混合持久化模式下安全备份的核心步骤:

#!/bin/bash
REDIS_CLI="/usr/bin/redis-cli"
AOF_PATH="/var/lib/redis/appendonly.aof"
BACKUP_DIR="/backup/redis"
HASH_FILE="${BACKUP_DIR}/aof_backup.sha256"

# 1. 检查是否有重写正在进行
REWRITE_STATUS=$($REDIS_CLI INFO PERSISTENCE | grep aof_rewrite_in_progress | cut -d: -f2 | tr -d '\r')
if [ "$REWRITE_STATUS" = "1" ]; then
    echo "AOF rewrite in progress, waiting..."
    while [ "$REWRITE_STATUS" = "1" ]; do
        sleep 2
        REWRITE_STATUS=$($REDIS_CLI INFO PERSISTENCE | grep aof_rewrite_in_progress | cut -d: -f2 | tr -d '\r')
    done
fi

# 2. 触发一次新的重写,确保备份文件是最新紧凑版本
$REDIS_CLI BGREWRITEAOF
sleep 2
while [ "$($REDIS_CLI INFO PERSISTENCE | grep aof_rewrite_in_progress | cut -d: -f2 | tr -d '\r')" = "1" ]; do
    sleep 2
done

# 3. 检查重写结果
REWRITE_STATUS=$($REDIS_CLI INFO PERSISTENCE | grep aof_last_bgrewrite_status | cut -d: -f2 | tr -d '\r')
if [ "$REWRITE_STATUS" != "ok" ]; then
    echo "AOF rewrite failed, aborting backup"
    exit 1
fi

# 4. 拷贝文件
TIMESTAMP=$(date +%Y%m%d%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/appendonly_${TIMESTAMP}.aof"
cp $AOF_PATH $BACKUP_FILE

# 5. 校验备份文件 RDB 头部魔数
HEADER=$(xxd -l 5 -p $BACKUP_FILE)
if [ "$HEADER" != "5245444953" ]; then
    echo "Invalid RDB header in backup file, aborting"
    rm -f $BACKUP_FILE
    exit 1
fi

# 6. 生成哈希
sha256sum $BACKUP_FILE > ${BACKUP_FILE}.sha256

echo "Backup completed: $BACKUP_FILE"

这个脚本的核心思路是:先确保没有正在进行的重写,然后主动触发重写并等待完成,确认重写成功后再拷贝文件,最后校验 RDB 头部魔数并生成哈希。在生产环境中,还需要加上错误重试、旧备份清理、加密传输等逻辑,但骨架就是这样。

混合持久化不是银弹,备份策略要匹配业务场景

混合持久化解决了 RDB 恢复快但丢数据多、AOF 数据全但恢复慢的矛盾,但它也引入了文件结构复杂、重写过程依赖子进程内存等新问题。备份策略必须围绕混合文件的特性来设计,不能照搬纯 RDB 或纯 AOF 时代的经验。如果你的业务允许分钟级的数据丢失,且数据量在几十 GB 以内,混合持久化加定期全量备份是比较省心的方案。如果数据量上百 GB 且对恢复时间有严格要求,就需要在备份频率、增量策略、网络带宽之间做更精细的权衡。

最后提醒一点:任何备份策略,如果没做过恢复演练,都等于零。混合持久化文件的恢复流程和纯 RDB、纯 AOF 有细微差别,建议每季度至少做一次从备份文件恢复的全流程演练,记录恢复耗时和步骤,形成文档。真到故障时,你不会有时间去研究文件格式的细节。