Redis混合持久化深度拆解:RDB前缀+AOF增量的恢复机制与实战
2026/9/8 16:29:09 网站建设 项目流程

先从一个实际场景说起。我之前维护过一套 Redis 集群,数据量到了十几个 G,业务端一直走的纯 AOF 持久化。有一回机房断电后重启,Redis 在那安安静静地重放了大概二十多分钟的写命令,期间所有请求全部超时,值班同事急得直找我。后来我把持久化方案改成混合持久化,同样是断电恢复,重启时间从二十分钟缩短到一分钟以内。这个差异的关键就在于 Redis 加载的持久化文件结构不一样了——AOF 文件前半段是 RDB 快照,后半段才是增量命令。很多朋友听说过“混合持久化”这个词,也知道大概是把 RDB 和 AOF 拼在一起,但问到“Redis 到底怎么识别这个文件、怎么把两种格式衔接起来”,能讲清楚的人其实不多。

这篇文章我就从混合持久化文件的组织结构入手,把前段 RDB、后段 AOF 的边界、Redis 的启动加载流程、我实际验证过的一些手段,以及这几年踩过的坑完整拆一遍。

1. 什么场景逼出了混合持久化:先复盘 RDB 和 AOF 两个老方案

1.1 纯 RDB:数据全量加载快,但丢数据的窗口太大

先梳理一下背景。Redis 最早只有 RDB 持久化,它做的事情很简单:定期把内存里的全量数据做一次二进制快照,写到 dump.rdb 文件里。这东西最大的优点是加载速度快,因为文件里存的就是内存数据结构序列化之后的结果,Redis 启动时只需要把这份快照反序列化回内存就行。对一个大几 G 的实例来说,RDB 恢复通常几十秒内可以完成。

缺点也很明显,RDB 的触发方式是“快照”,两次快照之间如果宕机了,中间所有写入的数据就全部丢失。默认配置是 900 秒内至少 1 次写操作、300 秒内至少 10 次、60 秒内至少 10000 次才触发一次快照,某些流量小的业务,或者还没到触发阈值的时候,最坏情况下可能要丢十几分钟的数据。这在很多核心业务里是根本没法接受的。

1.2 纯 AOF:命令攒着不丢,但重放命令慢、文件容易膨胀

为了缓解丢数据的问题,Redis 后来加入了 AOF。它的思路是把每一条写命令以 RESP 协议格式追加到 appendonly.aof 文件末尾。只要配置了 appendfsync everysec,最多只会丢 1 秒的数据。理论上数据安全度比 RDB 高很多。

可是纯 AOF 有个致命的毛病:启动恢复时要逐条重放命令。假如线上实例写入了 5000 万条写命令,恢复过程就是把这几千万条命令一条一条重新执行一遍。我当时遇到过线上 Redis 数据量只有几百 G?其实内存才十几个 G,但纯 AOF 文件因为历史积累能膨胀到几十 G,重启一次的时间从十几分钟到半小时不等。另一个问题是 AOF 文件会无限增长,如果长时间不重写,文件体积会远超实际数据量,对磁盘空间也是压力。

1.3 混合方案的设计逻辑:用 RDB 当 AOF 的“快进前缀”

纯 RDB 加载快但丢数据多,纯 AOF 丢数据少但加载慢,能不能把两者结合?这就是 Redis 4.0 引入的混合持久化。核心思路很简单:做一次 AOF 重写时,不再把所有历史命令都写到新文件里,而是先以 RDB 格式把当前内存中的全量数据写进去,等这份 RDB 数据流写完,再把重写期间产生的增量写命令以传统 AOF 命令流的形式继续追加到后面。这样生成的文件就叫“混合持久化文件”,它的结构就是标题里说的「前半段 RDB,后半段命令」。

你可以这么理解:RDB 段相当于一张带全部数据的“底表”,AOF 段相当于“底表之后的新增变更”。恢复时先快速加载底表,再顺着变更日志追平最后一段,速度和可靠性两头都兼顾。这个设计后来也成为 Redis 7.0 多部件 AOF 的雏形思想。

2. 混合持久化文件长什么样:前半段RDB和后半段命令的边界在哪

2.1 文件开头的 REDIS 魔数,是整个判断的分水岭

要理解 Redis 怎么加载混合文件,首先得知道它在文件开头放了什么东西。任何一个正常生成的 RDB 格式数据流,文件头一定是 5 个 ASCII 字符REDIS,紧接着是 4 位数字字符的 RDB 版本号,比如 Redis 7.x 常见的是REDIS0011。这个REDIS就是 RDB 格式的魔数签名,类似 ELF 文件开头的\x7fELF、PNG 图片开头的\x89PNG

混合持久化文件的前半段就是一段完整的 RDB 数据流,所以整个混合文件的第 1 个字节同样是R。Redis 启动加载 AOF 文件时,只要读一眼前 5 个字节是不是REDIS,就能立刻判断出这个文件到底是纯 AOF 命令文件还是混合持久化文件。这个判断在源码里非常直接:不是REDIS开头就按传统 AOF 命令流逐条解析;是REDIS开头就切到 RDB 加载流程。理解了这一点,后面加载流程就很好懂了。

2.2 前半段 RDB 的组成

前半段 RDB 部分和普通 dump.rdb 文件的内部结构没有本质区别,都是由几个区段拼接而成。

文件头部是刚才说的魔数 + RDB 版本号,比如REDIS0011。紧接着是辅助字段区,里面会记录一些元信息,比如 Redis 版本号、当前进程 ID、RDB 文件占用的内存大小、最后一次保存的写命令条数等。然后就是真正的数据区,Redis 实例里每个非空数据库会先有个数据库选择标记,后面跟着这个库里的所有键值对。每个键值对不只是存 key 和 value,还会带上对应的数据类型、过期时间、LRU 信息等元数据。数据写完以后,RDB 数据流会以一个专门的 EOF 标记收尾,最后是 8 字节 CRC64 校验和,用来保证从文件头到 EOF 这一段数据没有损坏。

所以混合文件的前半段其实是一段“有头有尾、自带校验”的完整 RDB 流。很多人在分析文件时以为 RDB 段只是简单地把键值对二进制堆到文件前面,实际上这段数据是封闭的,有明确的结束标记和校验信息,这为后面加载器确定切换位置提供了便利。

2.3 后半段 AOF 命令流从哪个位置开始

关键来了,混合文件的 AOF 部分从哪里开始?答案就藏在 RDB 段的结束标记后面。加载 RDB 段时,只要读到 EOF 标记并完成 CRC 校验,这一段的使命就完成了。文件指针继续往下读,剩余的内容全部都是传统 AOF 命令流。

后半段的命令流和纯 AOF 文件里的写法完全一样,每一条写命令都是 RESP 协议格式。我举一个例子,比如执行SET username zhangsan,在 AOF 文件里会变成这样:

*3\r\n$3\r\nSET\r\n$8\r\nusername\r\n$8\r\nzhangsan\r\n

人眼直接看是带\r\n的文本,本质上就是*3表示后面有三个命令参数,$3表示接下来的字符串长度是 3 个字节,然后是SET本身,以此类推。后半段里还可能夹杂着SELECT指令,用来切换这次重放要写入的数据库编号,保证多库场景下命令能够落到正确的库。

也正因为后半段是可读文本协议,你用strings命令查看混合持久化文件时,通常能看到一串以SELECTSETDELEXPIRE等开头的命令文本,这些就是前半段 RDB 结束之后追加进来的增量命令。

2.4 配置参数与重写触发

混合持久化不是 Redis 默认开启的,需要同时满足两个条件。

appendonly yes aof-use-rdb-preamble yes

appendonly yes是开启 AOF 持久化的总开关,aof-use-rdb-preamble yes是开启“在 AOF 重写时写入 RDB 前缀”的开关。需要注意,aof-use-rdb-preamble不是立刻改变现有 AOF 文件格式,而是要等到下一次 AOF 重写(BGREWRITEAOF)才会生成混合格式文件。具体触发的场景包括:手动执行BGREWRITEAOF、AOF 文件体积超过auto-aof-rewrite-percentageauto-aof-rewrite-min-size的阈值、主从全量同步时从节点触发重新生成文件等。

这里有一个很多人忽视的点:只要没触发重写,当前正在使用的 appendonly.aof 还是老的纯命令格式也没关系,Redis 启动加载时能在同一个逻辑里兼容两种形态。后面我会写加载流程,逻辑上就清楚了。

3. Redis 加载混合文件的全过程:从读魔数到命令重放

3.1 启动加载入口:为什么只有一个文件也能分清格式

Redis 启动时如果检测到appendonly yes,就会进入 AOF 加载流程。传统 AOF 加载逻辑是按行读取命令、逐条执行。混合持久化引入后,这段逻辑不能简单地按老办法走,因为文件前半段是二进制 RDB 数据,按文本行读取必然解析失败。Redis 的做法是先读文件开头 5 个字节做一个快速分拣。

伪代码角度大致是:

open appendonly.aof read 5 bytes if bytes == "REDIS": 按 RDB 格式加载整个前半段 继续加载剩余的命令段 else: 从文件头开始按纯 AOF 命令流逐条重放 close file

从 Redis 源码角度看,核心入口在 aof.c 的loadSingleAppendOnlyFile流程里。它会先检测文件头是不是 RDB 魔数,是的话就用 RDB 加载器处理前缀部分,处理完再回转到 RDB 结束位置,把后半段交给常规的 fake client 命令重放逻辑。这个设计让用户完全不需要手动指定“这个文件是混合的”这个事实,Redis 自己就能判断。

3.2 RDB 段加载阶段:重建全量键值对

一旦确认文件是REDIS开头,Redis 会进入 RDB 加载流程。它会先校验 RDB 版本号是否在当前实例支持范围内,然后开始反向解析整个 RDB 数据流。解析过程中遇到数据库选择标记,就把加载上下文切换到对应数据库;遇到普通键值对,就在对应库里构造对应数据类型;遇到带过期时间的键,就把过期时间和键一起恢复。

这个阶段是整段加载里性能最高的部分。因为 RDB 数据流在生成时就是按照内存数据结构的序列化结果写入的,加载器只需要反序列化,不需要像执行写命令那样做完整的命令查询、权限判断、键空间检查、慢日志记录之类的路径。所以多个 G 的数据,RDB 段往往几十秒内可以加载完成。如果 RDB 生成时启用了压缩,加载时会解压数据,这个动作会消耗一些 CPU,但依旧比逐条执行命令快得多。

3.3 RDB 段结束位置如何定位

RDB 段不是随便跑到哪里就结束,它有一个明确的结束标记。当加载器读到 RDB 自己的 EOF 标记时,就说明 RDB 数据区结束了,后面还会紧跟着 8 字节的 CRC64 校验和。Redis 会读取这段校验值,同时对从文件头部一直到 EOF 标记前的所有字节做一次 CRC64 计算,比对是否一致。这一步能有效发现磁盘损坏、文件被截断或者被篡改的问题。

校验通过后,RDB 加载完成,文件指针此时停留在 RDB 段之后、AOF 命令段的起点。Redis 接下来会把加载状态从“RDB 模式”无缝切换到“AOF 命令重放模式”。切换过程对内存数据没有任何特殊操作:RDB 段已经把全量键值对构建好了,AOF 段只需要把后面的增量命令逐条执行到这份已经存在的内存数据上。你可以把它理解成“先恢复底表,再追日志”。

3.4 AOF 段重放阶段:逐条命令应用到内存

切换完成后,Redis 会按照传统 AOF 加载方式一条一条读取剩余内容。实现上和正常客户端发命令没有本质区别,只是通过一个 fake client 模拟客户端执行。加载器按 RESP 协议从文件里解析出一条完整命令,比如SET key value,然后走标准命令执行链路写入内存。

这里你会看到“后半段命令”的几个重要特征。首先,它是追加在 RDB 完成之后的,所以理论上这段命令里出现的每一个 key,RDB 段里的事后状态已经存在;这些命令执行后会把实例更新到接近宕机前的状态。其次,因为 AOF 段里可能涉及多个数据库,Redis 在加载 AOF 段时如果遇到SELECT命令,会切换当前 fake client 的目标数据库。

整段 AOF 命令重放到底是快还是慢,取决于后半段命令条数。如果两次 AOF 重写之间间隔很短,后半段可能只有几百条命令,恢复只在毫秒级;如果触发重写不频繁,后半段可能有几百万条命令,这部分耗时就会比较明显。这也是为什么运维上要设置合理的 AOF 重写阈值,避免增量命令段无限膨胀。

3.5 加载收尾与通知数据就绪

所有命令都重放完毕后,Redis 会关闭文件,结束加载流程,然后进入正常的网络事件循环开始对外提供服务。启动日志里会输出类似DB loaded from append only file: xxx seconds的信息,有经验的运维可以通过这条日志判断加载耗时。此时客户端连上来,看到的数据已经是恢复完成后的完整状态了。

在线环境如果想观察加载进度,可以在 Redis 还在加载时用redis-cli info persistence查看loading状态,如果返回loading:1,说明数据还没加载完,这时 Redis 不会处理普通读写请求。很多监控系统就是用这个字段判断 Redis 是否处于启动恢复期。

4. 实操验证:手动打开混合持久化文件,一帧一帧看

4.1 准备一个能生成混合文件的 Redis 实例

纸上谈兵讲再多,不如自己亲手验证一遍。我建议在一台测试机上装一个全新的 Redis,然后按下面的步骤操作。

先在 redis.conf 里确认如下配置:

appendonly yes appendfilename "appendonly.aof" appendfsync everysec aof-use-rdb-preamble yes

启动 Redis 后,写入一批数据:

redis-cli 127.0.0.1:6379> SET user:1 zhangsan 127.0.0.1:6379> SET user:2 lisi 127.0.0.1:6379> SET user:3 wangwu 127.0.0.1:6379> RPUSH list:1 a b c d

然后手动触发一次 AOF 重写:

redis-cli BGREWRITEAOF

等重写完成后,appendonly.aof就会变成混合格式。注意看文件大小,里面不再是一行行纯文本命令,而是前半段一堆二进制,后半段才是可读的文本命令。

4.2 用 xxd 看魔数:文件头是怎么写的

这一步最关键,我一般用xxd看文件前几个字节:

xxd appendonly.aof | head

正常输出会类似:

00000000: 5245 4449 5330 3031 3105 0d0a fe00 ... REDIS0011......

看到没有,文件开头前 13 个字节里的52 45 44 49 53对应的 ASCII 字符正好就是REDIS,后面跟着30 30 31 31也就是0011。这就是 RDB 格式的铁证。如果是一个纯 AOF 命令文件,开头绝不会出现REDIS,而是直接以*开头的 RESP 协议,也就是十六进制的2a。所以 Redis 只要检查开头的 5 个字节,就能判断自己拿到的是哪种文件。

4.3 用 strings 看后半段命令流

RDB 段是二进制数据,直接cat会满屏乱码。想确认后半段是不是一堆命令文本,用strings最方便:

strings appendonly.aof | tail -n 50

如果文件生成后还有增量写命令,你会看到类似这样的输出:

SELECT 0 SET user:1 zhangsan SET user:2 lisi RPUSH list:1 a b c d

这个位置已经进入 AOF 命令段。比如RPUSH list:1这组命令,说明 AOF 重写期间之后又执行了对列表的写操作,所有这些操作都以追加式命令流的形式出现在 RDB 段后面。

为了进一步验证“后半段确实是文本命令流”,我做过一个比较野的实验:AOF 重写完成后,停掉 Redis,备份原文件,然后向文件尾部手动追加一条合法的 RESP 命令:

printf '*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n' >> appendonly.aof

再次启动 Redis,然后执行:

redis-cli GET hello

输出的结果很干净:

"world"

这条命令能生效的原因很简单:RDB 段本身的 CRC 校验只覆盖到 RDB 段结束位置,不会校验后面追加的内容;而 AOF 段加载是按 RESP 协议逐条读到文件末尾的,没有整体校验和,因此尾部追加的命令同样会被正确执行。这个实验能直观证明混合文件的边界和加载行为,不需要改任何源码就能读懂整个结构。需要提醒一句:这个操作只建议在测试环境做验证,生产环境的 AOF 文件千万别手动改,除非你提前做过完整备份。

4.4 用 redis-check-aof 检查文件完整性

Redis 自带的redis-check-aof工具可以用来对 AOF 文件做完整性检查。如果文件是混合格式,工具会识别出 RDB 前缀,并继续检查后面的命令段:

redis-check-aof appendonly.aof

完整无损的文件一般会输出类似:

AOF analyzed: size=xxx, ok_up_to=xxx, ok_up_to_line=xxx AOF is valid

如果我把刚才手动追加的命令里故意改坏一个字节,比如把SET改成SEX,再跑一遍检查,工具就会在出错位置报错,并给出合法命令无法解析的提示。这类检查对恢复数据前的文件健康度判断很有用,我一般会在每次计划重启前跑一次,防止启动中途卡在加载阶段。

5. 踩坑记录:加载混合文件时的高频故障与排查清单

5.1 bad file format / CRC 错误怎么排查

混合 AOF 文件恢复过程中最常见的错误有两种。一种是在 RDB 段报错,比如日志里出现Bad file format reading the append only file,而且错误位置在一些二进制区块上;另一种是CRC mismatch,说明 RDB 段的校验和和实际内容不一致。

遇到这两种情况,第一反应应该是怀疑磁盘损坏或者文件被部分覆盖,而不是立刻认为是代码缺陷。排查顺序我一般是这样:先复制一份文件出来,用redis-check-aof检查;再把错误位置附近的十六进制内容导出来看看;如果确认数据非常关键,急需恢复,就考虑先修文件再启动。最坏的情况下,直接从上次 RDB 备份或从节点把数据导回,接受丢失一小段窗口数据的代价。

5.2 文件末尾截断:aof-load-truncated 的默认行为

另一种高频场景是突然断电导致 AOF 文件末尾被截断。Redis 配置里有一个aof-load-truncated参数,默认值是yes,意思是启动加载时如果发现 AOF 文件末尾只有半截命令,Redis 会把它当成崩溃残留直接截断忽略,同时打印一条 warning,然后正常启动。

我自己遇到过很多次这样的情况:机房断电后重启 Redis,第一眼看到日志里有个警告很慌,但实际上 Redis 已经自动兜底把不完整的尾部跳过了。如果业务对数据完整度要求非常高,可以把aof-load-truncated改成no,这样 Redis 遇到截断文件会拒绝启动,避免在一份已经损坏的文件上继续写入造成二次污染。这个参数建议在核心业务上设成no,配合完善的备份体系,让“人工介入恢复”成为必经环节。

5.3 redis-check-aof --fix 到底能救回多少数据

当 AOF 文件损坏导致 Redis 无法启动时,很多人第一反应就是跑redis-check-aof --fix appendonly.aof。这个工具的修复策略本质上是扫描文件,找出一条命令解析失败的位置,然后让你选择是否把该位置之后的内容全部截断掉。也就是说,它能保住损坏点之前的所有命令。

注意一个边界:如果损坏点出现在 RDB 段内,修复工具的截断策略就可能把整个 RDB 前缀切掉,到时候能恢复出的数据量就不乐观了。我的经验是先备份文件,再跑 fix,恢复完立刻找一个临时实例验证加载结果,验证通过后再替换到生产路径上。千万不要在生产环境的原文件上直接 fix,万一修坏了就是二次事故。

5.4 版本升级与 RDB 版本兼容问题

混合文件的前半段是 RDB 格式,因此同样受 RDB 版本兼容性约束。Redis 6.x 生成的 RDB 文件版本号通常比 Redis 4.x 高,Redis 7.x 又比 6.x 高。理论上新版本 Redis 可以加载旧版本 RDB,因为 RDB 文件兼容策略原本就是向前的,但旧版本 Redis 去加载新版本 RDB 就会报版本不支持的错误。

所以,把 Redis 从高版本降级到低版本时,要注意混合 AOF 文件里的 RDB 段可能无法被低版本识别。我有一个习惯:跨大版本升级后,一定要在低版本实例上验证一次数据可恢复性,验证失败就说明降级路径走不通。混合文件让 AOF 里夹杂了 RDB 版本信息,很多人很容易忽略这个问题,等到真正做版本回退时才发现恢复不了,那时候处理成本就高了。

5.5 加载耗时变长,怎么定位瓶颈

如果混合文件恢复时间越来越长,要分两种情况排查。第一种是 RDB 段变大:说明当前键值对总量增长,瓶颈在 RDB 数据的加载和反序列化速度。第二种是 AOF 段变大:说明两次 AOF 重写间隔太长,增量命令积压太多,瓶颈在逐条重放命令的耗时。

定位方法也很直接:先看启动日志里DB loaded from append only file的总耗时,再用strings看 AOF 命令段的最后部分,比如最后一条命令的写入时间戳基本可以判断这段增量命令覆盖了多长的时间窗口。一旦 AOF 段覆盖了几个小时甚至几天的所有写入,就应该调小auto-aof-rewrite-percentage或手动定期执行BGREWRITEAOF。混合持久化虽然把恢复速度优化了一个量级,但不代表可以无限放纵 AOF 段膨胀,合理控制重写频率依然是关键手段。

我在实际排查中最后想强调的一点

聊到这,基本原理和运维方法都说清楚了。混合持久化文件本质上就是“用 RDB 做底、用 AOF 追尾”的结构,加载时的分水岭就在文件头部那 5 个字节的REDIS魔数上。读懂了这次切换,你不仅能回答常见的面试题“AOF 混合文件怎么加载”,也能在真实事故里快速判断:文件报错是坏在 RDB 段还是 AOF 段,启动慢到底是全量数据拖慢的还是增量命令拖慢的。

我个人这两年实践下来的体会是:混合持久化确实是我在生产环境里最推荐的一种持久化方案,但前提是配套好 AOF 定时重写、启动前文件检查、版本兼容预演这三件套。没有这套兜底,任何一个环节失控都可能抵消混合格式带来的恢复效率优势。下次如果你的 Redis 也遇到启动恢复慢的困扰,不妨先用xxd看一眼前 13 个字节,确认一下文件确实是以REDIS开头,再按文章里的思路去定位问题,排查路径会比你想的清晰很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询