数据库基础体系 · 第 36/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

Redis 持久化与内存:RDB、AOF、过期淘汰、Fork 和恢复

Redis 把数据主要保存在内存中,因此“进程重启后还能恢复什么”和“运行期间实际会占用多少内存”是两个相关但不同的问题:

  • RDB、AOF解决的是数据如何落到磁盘,以及重启时如何重建内存数据集。
  • 过期决定某个键在语义上何时失效,以及它何时被实际删除。
  • 淘汰决定达到 maxmemory 限制后,Redis 是否、以及如何主动删除仍然有效的键。
  • Fork是后台生成 RDB 或执行 AOF 重写时使用的进程机制,也是持久化期间内存峰值和延迟的重要来源。
  • 恢复是 Redis 启动时从 RDB 或 AOF 文件重新构造数据集的过程,不等同于复制、备份或故障转移。

本文以单个 Redis 实例的持久化和内存语义为主。复制、Sentinel、Cluster 可以改变故障转移路径,但不能把副本自动变成持久化备份;具体部署边界会在相关位置说明。


一、先区分四个时间点:写入、失效、删除、落盘

理解 Redis 持久化和内存,不能只看“命令执行成功”。一个键至少存在以下几个时间点:

  1. 命令执行时间:Redis 主线程修改了内存中的数据结构。
  2. 过期时间到达时间:如果键设置了 TTL,逻辑上的有效期结束。
  3. 实际删除时间:键被主动访问、过期扫描或淘汰策略删除。
  4. 持久化确认时间:修改对应的 RDB 或 AOF 数据已经按相应机制写入文件,并可能完成 fsync

这几个时间点不必相同。

例如:

SET session:42 abc EX 60

假设命令在 12:00:00 执行:

  • 12:01:00 之后,该键已经过期,不应再被正常读取;
  • Redis 可能在读取它时删除,也可能由后台过期循环删除;
  • 如果 Redis 在 12:00:30 生成了 RDB,RDB 中可能包含这个键及其剩余 TTL;
  • 如果 Redis 在 12:01:10 生成 RDB,通常不会把已经过期的键作为有效数据保存;
  • 如果 AOF 记录了 SET ... EX 60,重启时会根据写入时刻和剩余时间恢复其有效期,而不是永久恢复这个键。

因此,持久化文件不是简单的内存字节转储。它还必须表达过期时间、事务边界以及恢复时的执行语义。


二、RDB:某一时刻的数据集快照

2.1 RDB 的定义

RDB(Redis Database)是 Redis 的二进制快照格式。它描述某个时间点附近的数据集状态,通常包含:

  • 键和值;
  • 数据类型及其编码所需的信息;
  • 键的绝对过期时间;
  • 数据库编号;
  • 文件校验信息等。

RDB 不是逐条记录每次写入,而是把一个数据集编码成单个快照文件。其核心特点是:

  • 文件紧凑;
  • 适合冷备份和快速传输;
  • 恢复通常比重放大量命令更快;
  • 两次快照之间的修改可能丢失。

2.2 SAVEBGSAVE

Redis 提供两种典型的 RDB 生成方式:

SAVE
BGSAVE

SAVE 在主进程中同步生成快照。生成期间,Redis 不能正常处理其他命令,因此通常不适合生产在线实例。

BGSAVE 会创建子进程,由子进程生成 RDB,父进程继续处理客户端请求。典型流程如下:

父进程:
  1. 接收 BGSAVE
  2. fork 子进程
  3. 继续处理客户端命令

子进程:
  4. 遍历 fork 时可见的数据集
  5. 写入临时 RDB 文件
  6. 完成后替换目标 RDB 文件
  7. 退出

父进程:
  8. 记录后台保存成功或失败状态

RDB 的一致性边界是 fork 成功时的进程地址空间视图。父进程在子进程写快照期间继续处理命令,但这些后续修改不会被当前这次快照包含。

例如,数据变化如下:

10:00:00  数据集为 {a=1}
10:00:01  执行 BGSAVE,fork 完成
10:00:02  SET a 2
10:00:03  SET b 3

这次 RDB 看到的通常是:

{a=1}

而不是:

{a=2, b=3}

下一次 RDB 或 AOF 才可能包含后续修改。

2.3 配置触发的 RDB

Redis 可以通过 save 配置在满足“经过一定秒数且至少发生一定次数修改”时触发后台保存。例如:

save 900 1
save 300 10
save 60 10000

这些规则的含义是:

  • 900 秒内至少发生 1 次修改;
  • 或 300 秒内至少发生 10 次修改;
  • 或 60 秒内至少发生 10000 次修改;

满足任意一条,就可以触发一次后台 RDB 保存。它不是“每隔固定时间强制保存”,而是由时间窗口和修改计数共同决定。

可以用以下命令查看运行时配置和状态:

redis-cli CONFIG GET save
redis-cli INFO persistence

典型的 INFO persistence 中会关注:

rdb_bgsave_in_progress:0
rdb_last_bgsave_status:ok
rdb_last_save_time:...
rdb_changes_since_last_save:...

字段值和版本有关,诊断时应以当前版本输出为准。重点是确认:

  • 是否正在生成 RDB;
  • 上次后台保存是否成功;
  • 最近一次保存时间;
  • 保存后又积累了多少修改。

2.4 RDB 的丢失窗口

假设 Redis 在时间 t0t_0 成功生成了 RDB,之后发生了 NN 次写入,而在下一次 RDB 之前进程突然丢失:

RDB 快照:包含 t0 时刻的数据
t0 之后:写入 w1, w2, ..., wN
突然断电

如果没有 AOF,这些写入通常无法从 RDB 恢复。因此 RDB 的核心风险不是“文件损坏时一定无法恢复”,而是:

RDB 只保存快照时刻的数据,快照之后尚未进入其他持久化载体的修改可能丢失。

2.5 RDB 的优点和边界

RDB 适合:

  • 定期冷备份;
  • 快速复制或传输;
  • 允许丢失最近一段时间写入的缓存型数据;
  • 需要较紧凑备份文件的场景。

RDB 不适合单独承担:

  • 要求每次写入都尽量持久化的关键数据;
  • 需要精确恢复到最后一条命令的场景;
  • 把“生成了 RDB”误认为“磁盘和远端备份都已安全保存”。

特别要区分:

  • RDB 文件由 Redis 成功写出;
  • 文件内容已写入操作系统页缓存;
  • 文件已经 fsync 到存储设备;
  • 文件已经复制到独立故障域。

这些不是同一个保证。


三、AOF:通过重放写操作恢复数据

3.1 AOF 的定义

AOF(Append Only File)记录导致数据集变化的 Redis 操作。Redis 重启时,会从头读取 AOF,并按记录顺序重新执行这些操作。

概念上,若写入序列为:

SET user:1 alice
SADD online user:1
EXPIRE user:1 3600

AOF 保存的是能够表达这些操作的 RESP 命令记录,而不是某个时刻所有键的直接快照。

因此,AOF 的恢复模型是:

空数据集
  -> 执行第 1 条记录
  -> 执行第 2 条记录
  -> 执行第 3 条记录
  -> 得到恢复后的数据集

AOF 可以保留比 RDB 更近的写入,但代价通常是:

  • 文件可能增长更快;
  • 恢复需要解析和执行更多记录;
  • AOF 重写和 fork 也会带来 CPU、磁盘和内存压力。

3.2 appendfsync 的三种常见模式

AOF 追加后,Redis 还要决定何时要求操作系统把数据同步到存储设备。常见配置为:

appendonly yes
appendfsync everysec

主要模式如下:

配置 含义 典型风险
always 每个写操作都尽量执行同步 持久化窗口小,但写入延迟和 I/O 压力更高
everysec 通常每秒同步一次 机器故障时通常可能丢失约一秒级写入,但极端 I/O 阻塞时窗口可能扩大
no 由操作系统自行决定何时刷盘 性能和丢失窗口依赖操作系统及存储设备

这里的“同步”不能解释为绝对的物理介质保证。Redis 调用操作系统的同步接口后,具体存储设备、控制器、虚拟化层是否真正持久化,还取决于底层系统。

everysec 也不是精确的“最多丢失 1 秒”。如果后台 I/O 长时间阻塞,AOF 缓冲区可能不能及时同步,实际故障窗口会更大。

查看当前配置:

redis-cli CONFIG GET appendonly appendfsync
redis-cli INFO persistence

3.3 AOF 记录什么,不记录什么

AOF 记录的是改变数据集的操作,而不是所有客户端命令。例如:

GET user:1

不会因为读取而改变数据集,通常不需要写入 AOF。

而以下命令会修改数据集或相关元数据:

SET user:1 alice
INCR counter
EXPIRE user:1 60
DEL user:1

当键因过期而被删除时,Redis 也必须让恢复后的实例不会错误地把它恢复成永久有效键。主节点通常会把相应的删除语义写入 AOF,复制场景下也需要向副本传播删除语义。

3.4 事务与 AOF

Redis 事务通过:

MULTI
...
EXEC

将一组命令排队并一次执行。Redis 事务没有传统关系数据库那种回滚机制,但执行期间不会被其他客户端命令插入。

例如:

MULTI
SET account:1 90
SET account:2 110
EXEC

在单线程命令执行模型下,EXEC 中的命令会连续执行。持久化也必须保留足够的边界,使恢复不会把一个事务错误地拆成不可预期的状态。

一个重要边界是:

  • 事务执行前,语法错误等问题可能使事务无法正常执行;
  • EXEC 之后,某条命令因数据类型错误等运行时原因失败时,Redis 不会回滚已经执行的其他命令;
  • AOF 恢复的是实际产生的命令语义,而不是一个能自动回滚的数据库事务日志。

因此,不能把 Redis MULTI/EXEC 直接等同于支持回滚的 ACID 事务。

Lua 脚本和 Redis Functions 也必须考虑持久化语义:恢复时需要能够重现脚本产生的数据变化,而不是依赖客户端在恢复期间再次提交脚本。


四、AOF 重写:减少历史日志,而不是改变数据

4.1 为什么需要重写

AOF 是追加日志,长期运行后可能出现大量冗余:

SET counter 1
INCR counter
INCR counter
INCR counter

恢复时重放全部命令才能得到:

counter = 4

AOF 重写可以把当前数据集压缩成更短的等价命令,例如只保留:

SET counter 4

重写的目标是:

用当前数据集生成一份新的、语义等价但更紧凑的 AOF,而不是简单删除旧文件中的一段文本。

4.2 BGREWRITEAOF 的流程

可以手动触发:

redis-cli BGREWRITEAOF
redis-cli INFO persistence

典型流程:

1. 父进程 fork 子进程
2. 子进程遍历 fork 时刻的数据集
3. 子进程把当前数据集写成新的基础 AOF
4. 父进程继续接收写命令
5. 父进程把重写期间产生的修改暂存为增量内容
6. 重写完成后,把基础文件和增量修改合并为新的有效 AOF
7. 原有 AOF 文件被替换或由新格式的文件清单接管

较新的 Redis 版本使用多部分 AOF(Multi-Part AOF,简称 MP-AOF)组织基础文件和增量文件,并通过清单描述当前有效文件集合。不要假设 AOF 永远只有一个名为 appendonly.aof 的普通文件;具体文件布局取决于 Redis 版本和配置。

4.3 重写期间的并发一致性

假设 fork 时数据为:

{counter=100}

重写子进程开始后,父进程又执行:

INCR counter
SET flag on

子进程生成的基础 AOF 可能只表达:

SET counter 100

但父进程必须记录重写期间的新增修改。最终有效 AOF 需要相当于:

SET counter 100
INCR counter
SET flag on

恢复后得到:

{counter=101, flag=on}

如果没有这段增量记录,重写期间的写入就会丢失。因此 AOF 重写不是“暂停写入后导出”,而是“快照基础数据加上并发修改”。

4.4 AOF 重写不是备份

AOF 重写会替换当前正在使用的日志结构。它的主要目标是控制日志增长和恢复时间,不等于保留历史版本。

如果误执行:

DEL important:key

之后 AOF 重写,新的 AOF 可能只保留“当前不存在该键”的状态。重写不会帮助找回历史值。

需要历史恢复点时,应把 RDB 或 AOF 文件复制到独立位置,并保留版本化备份,而不是只依赖实例当前目录中的文件。


五、RDB 与 AOF 同时启用时如何恢复

通常可以这样配置:

save 900 1
save 300 10
save 60 10000

appendonly yes
appendfsync everysec

两者同时启用的作用不同:

  • RDB 提供相对紧凑的快照;
  • AOF 提供快照之后的增量修改;
  • AOF 重写时也可能生成一个基础快照,再附加重写期间的增量。

当 AOF 可用时,Redis 启动通常优先使用 AOF,因为它通常包含比 RDB 更新的状态。具体文件发现和多部分 AOF 的选择遵循当前版本的文件清单和目录结构。

这意味着:

RDB 里有 a=1
AOF 后续记录 SET a 2

恢复后应为:

a=2

而不是 a=1

但是,AOF 优先并不意味着 RDB 不重要:

  • AOF 可能损坏;
  • AOF 可能误删数据后被重写;
  • AOF 可能比 RDB 更大,恢复耗时更长;
  • RDB 可以作为独立的冷备份和灾难恢复来源。

六、过期:逻辑失效不等于立即回收内存

6.1 过期键的定义

过期键是带有绝对过期时间的键。例如:

SET cache:item value EX 60

EX 60 表示从命令执行时刻起约 60 秒后失效。也可以使用毫秒:

SET cache:item value PX 60000

查看 TTL:

TTL cache:item
PTTL cache:item

可能的结果包括:

  • 非负数:剩余时间;
  • -1:键存在但没有过期时间;
  • -2:键不存在,或已经不可见。

6.2 被动过期

当客户端访问一个已经过期的键时,Redis 会检查过期时间。如果发现已过期,通常会在返回结果前删除它,并把它当作不存在处理。

例如:

SET token abc EX 1
GET token

等待超过 1 秒后:

GET token

预期结果是:

(nil)

这个结果并不要求后台扫描已经提前把键从内存中清除。访问路径本身可以完成过期判断和删除。

6.3 主动过期

如果某个键过期后再也没有被访问,仅靠被动过期就无法回收它。因此 Redis 还会周期性抽样检查带 TTL 的键,主动删除已经过期的键。

主动过期具有两个重要特征:

  1. 它是后台周期性工作的近似过程,不是为每个键创建一个精确的定时器;
  2. 某些过期键可能在 TTL 到达后短时间仍占用内存,直到被访问或扫描到。

因此,下列说法是不准确的:

EXPIRE key 60 执行后,60 秒整内存一定立刻下降。

更准确的说法是:

60 秒后该键在语义上已经过期;实际内存释放取决于访问路径、主动过期扫描以及内存分配器的回收行为。

6.4 过期时间如何进入 RDB 和 AOF

RDB 保存过期时间。加载时,Redis 会根据当前时间判断键是否已经过期,过期键不会作为有效数据恢复。

AOF 可以记录设置过期时间的命令,也可以记录由过期导致的删除语义。恢复时,Redis 不应把一个已经过期的键错误地恢复成永久有效键。

过期时间通常以绝对时间语义表达,因此时间异常会影响行为。例如:

  • 生成 RDB 后,把文件复制到另一台机器;
  • 两台机器的系统时钟差异很大;
  • 加载 RDB 时,部分键可能立即被判定为过期。

所以,跨机器搬运 RDB 时,系统时钟同步是恢复正确性的前置条件之一。

6.5 过期与复制

在主从复制中,过期的权威删除通常由主节点决定并传播。副本不是简单地独立按照本地扫描结果删除所有键,否则主从可能因时钟和扫描时机不同而产生差异。

因此:

  • 副本的过期行为不能简单理解为“每个节点各自独立计时并立即删除”;
  • 主节点因过期删除键时,删除语义需要传播给副本;
  • 复制延迟期间,客户端从副本读取时还可能看到与主节点不同的短暂状态,具体还受读流量和部署方式影响。

这也是为什么复制不能替代持久化:复制的是运行中的状态和命令流,主节点误删、逻辑错误或共同故障都可能传播到副本。


七、淘汰:达到 maxmemory 后主动删除有效键

7.1 过期和淘汰的区别

过期是应用或命令明确给出的时间语义:

这个键到某个时间后不再有效。

淘汰是内存压力下的容量控制:

数据集超过内存策略允许的范围,需要删除某些仍然有效的键。

一个没有 TTL 的键也可能因为淘汰被删除;一个有 TTL 的键也可能在到期前被淘汰。

7.2 maxmemory 的基本模型

配置示例:

maxmemory 2gb
maxmemory-policy allkeys-lru

当 Redis 判断需要为新写入腾出空间时,会根据策略尝试删除键。策略主要分为以下几类:

策略 候选范围 选择方式
noeviction 不主动淘汰 写入无法完成时返回错误
allkeys-lru 所有键 淘汰近似最久未使用的键
volatile-lru 仅有 TTL 的键 淘汰近似最久未使用的键
allkeys-lfu 所有键 淘汰近似最不常使用的键
volatile-lfu 仅有 TTL 的键 淘汰近似最不常使用的键
allkeys-random 所有键 随机选择
volatile-random 仅有 TTL 的键 随机选择
volatile-ttl 仅有 TTL 的键 优先选择剩余 TTL 较短的键

volatile-* 策略的关键边界是:

如果没有足够多的带 TTL 键作为候选,Redis 不能从没有 TTL 的键中补充淘汰。

这可能导致写入失败。比如:

maxmemory-policy volatile-lru

数据集中只有永久键,没有任何 TTL。此时即使内存压力已经触发,也没有符合策略的候选键,写入可能返回类似:

(error) OOM command not allowed when used memory > 'maxmemory'.

7.3 LRU 和 LFU 通常是近似算法

Redis 的 LRU 淘汰通常不是维护一个精确的全局访问排序表,而是从候选键中抽样,再近似选择较久未访问的键。可以通过:

maxmemory-samples 10

调整抽样数量。抽样更多通常更接近理想 LRU,但也会增加淘汰路径的工作量。

LFU 也不是无限精度的访问计数器。它采用有限位宽和衰减机制,使频率能随时间变化,避免一个很久以前的热点键永久占据“高频”位置。

因此:

allkeys-lru

不保证每次都删除整个实例中严格最久未访问的键;它表达的是一种近似策略。

7.4 淘汰发生时的错误边界

淘汰主要发生在为命令分配新内存的过程中。不能把 maxmemory 理解为“后台线程持续把 RSS 精确维持在这个数值以下”。

以下因素都可能使系统实际内存高于 maxmemory

  • Redis 自身的非数据内存;
  • 客户端输入和输出缓冲区;
  • AOF 缓冲区;
  • 复制积压和复制缓冲;
  • 内存分配器碎片;
  • 后台 RDB 或 AOF 重写的 Fork/COW 开销;
  • 加载、解码和临时工作内存。

此外,删除键后,内存分配器可能把空间留在进程地址空间中,供后续复用,而不是立即还给操作系统。因此:

键数量下降

不一定立即导致:

RSS 同比例下降

八、Fork:后台持久化的地址空间快照机制

8.1 Fork 的基本语义

Unix-like 系统中的 fork 会创建一个子进程。父子进程开始时共享相同的内存页,但使用写时复制(Copy-on-Write,COW)机制:

  • 读共享页不会复制;
  • 任一进程首次修改某个共享页时,操作系统复制该页;
  • 修改方写入自己的副本;
  • 另一方仍看到原来的页内容。

Redis 使用这一机制让子进程遍历一个相对稳定的数据集,同时父进程继续处理请求。

8.2 RDB 和 AOF 重写为什么都需要 Fork

如果父进程一边修改哈希表、跳表或 Stream,一边由同一个执行流直接遍历并写快照,就需要复杂的并发协调。Redis 的单线程命令执行模型配合 fork,可以得到一个一致的视图:

父进程:继续处理客户端写入
子进程:读取 fork 时刻的数据集并生成文件

但这并不意味着“子进程复制了完整内存”。开始时大量页是共享的,只有被修改的页才因 COW 产生额外副本。

8.3 COW 的内存计算示例

假设:

数据相关内存:1 GiB
后台 BGSAVE 开始时 fork 成功
重写期间,父进程修改导致约 20% 的内存页发生 COW

粗略估算:

额外 COW 页 ≈ 1 GiB × 20% = 0.2 GiB

这不表示总 RSS 必然精确增加 0.2 GiB,因为还存在:

  • 页粒度和修改分布;
  • Redis 自身额外内存;
  • 分配器碎片;
  • 子进程写文件的缓冲;
  • 其他客户端和复制缓冲。

但它说明了一个常见现象:

持久化期间,父子进程共享页越多,修改越密集,COW 额外内存越大。

如果 fork 期间父进程持续大量写入,接近整个数据集的页都可能被复制。此时峰值可能接近:

原数据集 + 大量 COW 页 + 非数据缓冲 + 分配器开销

8.4 Fork 的 CPU 和延迟成本

fork 不会立刻复制全部数据,但需要复制和设置进程页表,成本与进程地址空间规模有关。因此:

  • 数据集越大,fork 本身可能越慢;
  • 大量页表操作可能造成主线程延迟抖动;
  • fork 后的高写入率会放大 COW 内存;
  • 如果系统内存不足,可能触发交换、分配失败或 OOM;
  • 后台子进程写磁盘会与 AOF、其他进程争用 I/O。

可以查看相关指标:

redis-cli INFO memory
redis-cli INFO persistence
redis-cli INFO stats

重点关注:

used_memory
used_memory_rss
used_memory_peak
mem_fragmentation_ratio
latest_fork_usec
rdb_current_bgsave_time_sec
aof_rewrite_in_progress
aof_last_bgrewrite_status

字段是否存在以及含义细节应以运行版本的 INFO 文档为准。latest_fork_usec 用于观察最近一次 fork 的耗时,不能直接等同于整次 RDB 或 AOF 重写耗时。

8.5 Fork 的反例:maxmemory 看起来足够,但仍然失败

假设:

maxmemory = 8 GiB
used_memory = 7 GiB
机器可用物理内存 = 7.2 GiB

从数据集角度看似还剩 1 GiB 空间,但此时启动后台重写:

  1. fork 需要页表和系统资源;
  2. 父进程继续写入并产生 COW;
  3. 子进程还要写文件;
  4. 分配器、客户端缓冲区和复制缓冲仍然存在。

如果 COW 和其他开销超过可用物理内存,系统可能出现严重抖动、fork 失败或 OOM。maxmemory 不是整台机器的内存安全上限,也不是持久化峰值预算。


九、RDB、AOF、过期和淘汰如何共同作用

下面用一个完整例子串起几个机制。

初始配置:

maxmemory 100mb
maxmemory-policy allkeys-lru
appendonly yes
appendfsync everysec

执行:

SET user:1 alice
SET cache:1 x EX 10
LPUSH jobs job-1

9.1 命令执行后的内存状态

内存数据集大致为:

user:1  -> "alice",无 TTL
cache:1 -> "x",TTL 约 10 秒
jobs    -> List["job-1"],无 TTL

同时,AOF 追加能够重建这些状态的命令。

9.2 生成 RDB

如果此时执行:

BGSAVE

fork 时的数据集包含三个键及 cache:1 的过期时间。RDB 子进程写出快照,父进程仍可执行新命令。

9.3 十秒后访问 cache:1

GET cache:1

如果过期时间已到,结果为:

(nil)

这次访问路径会识别并删除过期键。删除语义也需要被持久化或复制,以确保后续恢复和副本不会把该键重新当成有效数据。

9.4 内存超过 maxmemory

如果继续写入大量新键,Redis 需要腾出空间:

  • allkeys-lru 可以从所有键中选择近似较久未使用者;
  • cache:1 即使未到期,也可能提前被淘汰;
  • 被淘汰的键不等于过期键;
  • 如果策略是 noeviction,写命令可能直接返回 OOM 错误,而不是删除键。

9.5 持久化期间继续写入

如果 BGSAVE 子进程仍在写文件,父进程继续写入:

SET user:2 bob
INCR counter

这些修改不在当前 RDB 的 fork 视图中。若启用了 AOF,它们会进入 AOF;若仅启用 RDB,则要等下一次 RDB 才会包含它们。


十、恢复:Redis 启动时如何重建内存数据

10.1 恢复的基本路径

Redis 启动时会检查持久化文件,并将其加载到内存:

启动进程
  -> 检查 AOF 是否启用及其有效文件结构
  -> 若使用 AOF,则解析并重放 AOF
  -> 否则加载 RDB
  -> 重建字典、过期信息和各数据类型对象
  -> 开始接受客户端请求

恢复期间 Redis 通常不能像正常运行时一样服务普通请求,因此恢复时间直接影响重启后的不可用窗口。

10.2 RDB 恢复

RDB 恢复大致是:

  1. 打开 RDB 文件;
  2. 校验文件格式和内容;
  3. 按数据库编号读取键;
  4. 恢复值及过期时间;
  5. 对已过期键不作为有效数据加载;
  6. 完成后进入服务状态。

如果 RDB 在中间位置损坏,Redis 通常会报告加载失败,而不是静默生成一个可能缺数据的数据集。可以使用:

redis-check-rdb /path/to/dump.rdb

检查文件。实际二进制路径可能是发行版或 Redis 安装目录中的 redis-check-rdb

不要在生产数据目录上直接尝试修复原文件。更安全的流程是:

1. 停止或隔离实例
2. 复制损坏文件
3. 对副本执行检查
4. 从已验证的备份恢复
5. 在隔离端口启动验证
6. 验证业务关键键和 TTL
7. 再切换流量

10.3 AOF 恢复

AOF 恢复需要:

  1. 读取 AOF 文件或当前版本定义的多部分 AOF 文件集合;
  2. 解析 RESP 命令;
  3. 按顺序执行;
  4. 重建数据集和过期信息;
  5. 在完成后提供服务。

如果 AOF 只是在文件尾部留下了不完整命令,例如机器在写入一条命令的中途崩溃,Redis 通常可以识别这种尾部不完整情况,并根据相关配置选择截断尾部后继续加载。这个行为不应扩展解释为“任何 AOF 损坏都能自动修复”:

  • 尾部不完整:可能可以截断;
  • 中间位置损坏:通常不能安全忽略,因为后续协议边界和数据语义都可能被破坏。

检查工具示例:

redis-check-aof --fix /path/to/appendonly.aof

--fix 会修改文件,必须先做副本,并保留原始文件用于取证和回滚。对于多部分 AOF,不能机械地把某个旧版本单文件命令套用到新版本目录结构上,应使用与该 Redis 版本匹配的检查方式。

10.4 恢复后的 TTL 验证

恢复不能只验证键值,还要验证过期时间:

redis-cli -h 127.0.0.1 -p 6379 GET cache:1
redis-cli -h 127.0.0.1 -p 6379 PTTL cache:1

如果业务依赖 TTL,至少应检查:

键是否存在
值是否正确
TTL 是否仍为预期范围

例如,原本设置为 60 秒的缓存,在备份和恢复过程中已经过去 90 秒,那么恢复后它不应重新变成剩余 60 秒。把过期时间错误恢复成新的完整 TTL,会造成缓存污染、会话延长或安全问题。


十一、如何验证持久化,而不是只检查配置

看到:

appendonly yes

并不能证明数据已经安全落盘。应设计可重复的验证流程。

11.1 最小 RDB 验证

在测试实例中执行:

redis-cli FLUSHALL
redis-cli SET demo:rdb value
redis-cli SET demo:ttl value EX 120
redis-cli BGSAVE
redis-cli LASTSAVE
redis-cli INFO persistence

然后:

1. 停止实例
2. 保留生成的 RDB 文件
3. 在隔离目录启动另一个实例
4. 查询 demo:rdb
5. 查询 demo:ttl 的 PTTL

预期:

GET demo:rdb       -> "value"
PTTL demo:ttl      -> 一个接近 120000 毫秒但已扣除经过时间的非负值

测试时不要只验证普通字符串,还应覆盖:

  • Hash;
  • List;
  • Set;
  • ZSet;
  • Stream;
  • 大对象;
  • 带 TTL 的键;
  • 空集合和边界数据。

不同数据类型的编码和恢复路径不同,单个 SET 只能证明最简单的路径可用。

11.2 最小 AOF 验证

测试配置:

appendonly yes
appendfsync everysec

执行:

redis-cli FLUSHALL
redis-cli SET demo:aof 1
redis-cli INCR demo:aof
redis-cli MULTI
redis-cli SET demo:tx:a x
redis-cli SET demo:tx:b y
redis-cli EXEC

验证:

redis-cli GET demo:aof
redis-cli GET demo:tx:a
redis-cli GET demo:tx:b
redis-cli INFO persistence

然后停止实例、复制 AOF 文件到隔离目录,再重新启动并查询:

demo:aof   -> "2"
demo:tx:a  -> "x"
demo:tx:b  -> "y"

这个测试验证了:

  • 普通写入是否进入 AOF;
  • 增量操作能否正确重放;
  • 事务中的多个命令能否恢复为预期状态。

它仍然不能证明物理存储在断电时一定满足某个强保证。要验证断电语义,需要在与生产存储和文件系统相同的环境中进行故障注入测试。

11.3 备份验证必须包含“可启动性”

只复制文件而不尝试加载,无法证明备份可用。一个实际的恢复演练至少包括:

备份文件
  -> 校验文件完整性
  -> 在隔离实例启动
  -> 等待加载完成
  -> 验证关键键和值
  -> 验证 TTL
  -> 验证数据库数量
  -> 验证应用读写

恢复演练还应记录:

  • 文件大小;
  • 加载耗时;
  • 恢复后的内存占用;
  • 是否存在错误日志;
  • 应用是否能接受恢复后的数据状态。

十二、故障路径与诊断方法

12.1 写入突然返回 OOM

可能原因包括:

  1. maxmemory-policy noeviction
  2. volatile-* 策略下没有足够的带 TTL 候选;
  3. Redis 正处于内存峰值,实际可用内存不足;
  4. 持久化 Fork 和 COW 消耗了额外内存;
  5. 单个命令需要一次性分配大对象;
  6. 客户端、复制或 AOF 缓冲区增长。

诊断命令:

redis-cli INFO memory
redis-cli CONFIG GET maxmemory maxmemory-policy maxmemory-samples
redis-cli MEMORY STATS
redis-cli INFO persistence
redis-cli INFO clients
redis-cli INFO replication

不要只看:

used_memory < maxmemory

还要比较:

used_memory
used_memory_rss
used_memory_peak
mem_fragmentation_ratio

其中 used_memory 更接近 Redis 分配器管理的数据,RSS 是操作系统看到的进程驻留内存,两者可能因碎片、页表和其他缓冲而明显不同。

12.2 持久化期间延迟升高

可能路径是:

BGSAVE/BGREWRITEAOF
  -> fork 延迟
  -> 父进程写入造成 COW
  -> 内存压力增加
  -> 磁盘写入争用
  -> AOF fsync 延迟
  -> 主线程处理命令变慢

诊断时应同时看:

redis-cli INFO persistence
redis-cli INFO memory
redis-cli LATENCY LATEST
redis-cli SLOWLOG GET 20

SLOWLOG 主要记录 Redis 命令执行时间,不一定包含所有系统调用等待;LATENCY 和操作系统的磁盘、内存、调度指标需要结合分析。

12.3 启动时恢复很慢

常见原因:

  • AOF 文件很大;
  • AOF 中包含大量历史命令;
  • 数据类型对象复杂或数量很多;
  • 磁盘读取慢;
  • CPU 受限;
  • 恢复期间发生大量过期判断和对象分配。

AOF 重写可以降低未来恢复量,但重写本身也需要资源。不能简单地认为“启用 AOF 后恢复一定比 RDB 快”或“一定比 RDB 慢”;实际取决于文件规模、命令数量、数据编码和硬件。

12.4 RDB 或 AOF 文件损坏

正确处理顺序通常是:

1. 保留原始文件,不在其上直接修改
2. 查阅 Redis 启动日志,确认损坏位置和类型
3. 使用匹配版本的检查工具
4. 优先从最近的独立备份恢复
5. 若只有 AOF 尾部不完整,再评估截断
6. 在隔离实例验证后切换

不应把 redis-check-aof --fix 当作无损修复工具。它的本质可能是丢弃无法解析的尾部内容,因此修复后的实例状态可能缺少最后一部分写入。


十三、几个容易混淆的结论

13.1 “有副本,所以不需要持久化”

错误。副本可能:

  • 与主节点共享同一机架、可用区或存储故障域;
  • 复制了误删、错误更新或过期删除;
  • 在网络分区期间落后;
  • 在故障转移中丢失尚未传播的写入。

副本用于提高可用性和读取能力;RDB/AOF 用于进程重启和持久化恢复。两者解决的问题不同。

13.2 “AOF everysec 保证最多丢一秒”

不严谨。它表达的是 Redis 通常以约一秒为周期执行 AOF 同步,但实际丢失窗口受调度、磁盘延迟、操作系统和故障类型影响,极端情况下可能更大。

13.3 “设置了 TTL,内存一定自动按时下降”

错误。TTL 首先改变键的有效性。实际内存释放可能由:

  • 客户端访问触发;
  • 主动过期扫描触发;
  • 淘汰触发;
  • 分配器后续复用或归还操作系统。

即使键已删除,RSS 也不一定立即下降。

13.4 “maxmemory 就是进程 RSS 的硬上限”

错误。maxmemory 主要控制 Redis 数据集容量和淘汰行为,不能覆盖所有进程级内存成本。Fork/COW、客户端缓冲、复制缓冲、AOF 缓冲和分配器碎片都可能使实际 RSS 更高。

13.5 “后台 RDB 一定不会阻塞 Redis”

错误。父进程可以继续处理命令,但 fork 可能产生延迟,后台磁盘 I/O 可能争用资源,父进程写入还会触发 COW。它减少了长时间同步遍历对请求的阻塞,却没有消除持久化成本。

13.6 “AOF 重写后可以找回误删数据”

错误。重写保留的是当前状态,不是历史版本。要恢复误删数据,需要另有时间点备份、复制快照或审计日志。


十四、如何按数据重要性选择组合

选择持久化方式时,先明确允许丢失的数据范围。

14.1 可重建缓存

如果 Redis 中的数据可以从数据库或其他系统重新构建,可以使用 RDB 或较宽松的 AOF 配置,以降低持续写盘成本。即使丢失最近修改,业务也能接受缓存未命中。

14.2 需要较小丢失窗口的状态

可以启用 AOF,并根据延迟和数据重要性选择:

appendonly yes
appendfsync everysec

这通常在性能和持久化之间取得折中,但仍要通过实际存储测试确认故障窗口。

14.3 需要更强持久化要求的写入

可以考虑:

appendonly yes
appendfsync always

但这会显著增加同步调用和存储压力。是否满足业务要求,不能仅由配置名称判断,还需要验证:

  • 存储设备断电保护;
  • 文件系统和虚拟化层行为;
  • Redis 进程异常退出;
  • 主机断电;
  • 双机或多机故障;
  • 恢复后数据是否符合业务不变量。

14.4 大数据集实例

大实例尤其要同时预算:

数据集内存
+ 分配器碎片
+ 客户端与复制缓冲
+ AOF 缓冲
+ Fork 页表开销
+ COW 增量
+ 子进程写文件开销
+ 操作系统和其他进程

如果只按照 maxmemory 设置机器内存,后台持久化很容易成为内存峰值来源。RDB 周期、AOF 重写触发时机、业务写入峰值和磁盘吞吐应联合规划,而不是分别设置后再观察故障。


十五、用一个故障时间线理解最终状态

假设:

T0:成功生成 RDB,内容为 k=1
T1:执行 SET k 2
T2:AOF 追加了该命令,但尚未完成设备同步
T3:执行 BGSAVE,生成的新 RDB 是否包含 k=2 取决于 fork 时刻
T4:机器断电

可能出现不同结果:

只有 RDB

如果 T3 的快照在 SET k 2 之前 fork:

恢复 k=1

如果 T3 在之后 fork:

恢复 k=2

RDB + AOF,AOF 已经安全保留该写入

即使 RDB 仍是:

k=1

恢复时重放 AOF 后:

k=2

RDB + AOF,但写入只在内存或操作系统缓存中

如果断电导致这条 AOF 记录没有进入可恢复介质:

AOF 中可能没有 SET k 2

最终仍可能恢复为:

k=1

这说明持久化可靠性取决于完整数据流:

命令执行
  -> Redis 内存
  -> AOF/RDB 文件
  -> 操作系统缓存
  -> 存储设备
  -> 独立备份或故障域

只验证其中一个箭头,不能推出全部链路都可靠。


Redis 的持久化不是单一开关,而是一组相互作用的机制:

  • RDB 用快照表达某个时间点的数据集;
  • AOF 用可重放操作表达连续修改;
  • AOF 重写用当前状态压缩历史日志;
  • 过期使键在时间语义上失效,但不保证立即回收内存;
  • 淘汰在容量压力下删除仍可能有效的键;
  • Fork 让后台持久化拥有稳定视图,却引入页表、COW 和内存峰值;
  • 恢复过程决定这些文件最终如何重新变成可服务的数据集。

只有同时验证数据丢失窗口、过期时间、淘汰行为、Fork 峰值、文件完整性和实际恢复过程,才能判断一套 Redis 持久化配置是否真正符合业务要求。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。