数据库基础体系 · 第 36/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
Redis 持久化与内存:RDB、AOF、过期淘汰、Fork 和恢复
Redis 把数据主要保存在内存中,因此“进程重启后还能恢复什么”和“运行期间实际会占用多少内存”是两个相关但不同的问题:
- RDB、AOF解决的是数据如何落到磁盘,以及重启时如何重建内存数据集。
- 过期决定某个键在语义上何时失效,以及它何时被实际删除。
- 淘汰决定达到
maxmemory限制后,Redis 是否、以及如何主动删除仍然有效的键。 - Fork是后台生成 RDB 或执行 AOF 重写时使用的进程机制,也是持久化期间内存峰值和延迟的重要来源。
- 恢复是 Redis 启动时从 RDB 或 AOF 文件重新构造数据集的过程,不等同于复制、备份或故障转移。
本文以单个 Redis 实例的持久化和内存语义为主。复制、Sentinel、Cluster 可以改变故障转移路径,但不能把副本自动变成持久化备份;具体部署边界会在相关位置说明。
一、先区分四个时间点:写入、失效、删除、落盘
理解 Redis 持久化和内存,不能只看“命令执行成功”。一个键至少存在以下几个时间点:
- 命令执行时间:Redis 主线程修改了内存中的数据结构。
- 过期时间到达时间:如果键设置了 TTL,逻辑上的有效期结束。
- 实际删除时间:键被主动访问、过期扫描或淘汰策略删除。
- 持久化确认时间:修改对应的 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 SAVE 和 BGSAVE
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 在时间 成功生成了 RDB,之后发生了 次写入,而在下一次 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 的键,主动删除已经过期的键。
主动过期具有两个重要特征:
- 它是后台周期性工作的近似过程,不是为每个键创建一个精确的定时器;
- 某些过期键可能在 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 空间,但此时启动后台重写:
- fork 需要页表和系统资源;
- 父进程继续写入并产生 COW;
- 子进程还要写文件;
- 分配器、客户端缓冲区和复制缓冲仍然存在。
如果 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 恢复大致是:
- 打开 RDB 文件;
- 校验文件格式和内容;
- 按数据库编号读取键;
- 恢复值及过期时间;
- 对已过期键不作为有效数据加载;
- 完成后进入服务状态。
如果 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 恢复需要:
- 读取 AOF 文件或当前版本定义的多部分 AOF 文件集合;
- 解析 RESP 命令;
- 按顺序执行;
- 重建数据集和过期信息;
- 在完成后提供服务。
如果 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
可能原因包括:
maxmemory-policy noeviction;volatile-*策略下没有足够的带 TTL 候选;- Redis 正处于内存峰值,实际可用内存不足;
- 持久化 Fork 和 COW 消耗了额外内存;
- 单个命令需要一次性分配大对象;
- 客户端、复制或 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 持久化配置是否真正符合业务要求。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:Redis 数据结构完整指南:String、Hash、List、Set、ZSet 与 Stream
- 下一篇:Redis 复制、Sentinel 与 Cluster:槽位、故障转移和一致性
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论