场景:进程重启后数据去哪了
Redis 把数据放在内存里,进程一旦退出内存就清空。但生产里我们重启 Redis 后数据往往还在——这靠的是持久化:把内存状态落到磁盘,重启时回放。Redis 提供两套机制:RDB(快照) 和 AOF(追加日志),它们代表了两种截然不同的哲学:一个存"某一刻的全量状态",一个存"导致当前状态的所有操作"。理解二者的取舍,直接决定你能容忍多少数据丢失、重启要等多久。
RDB:某一时刻的内存全量快照
RDB 把某个时间点的整个数据集序列化成一个紧凑的二进制文件(dump.rdb)。触发方式有 SAVE(同步,阻塞主线程,基本不用)和 BGSAVE(后台异步)。配置里常见的:
1 | save 900 1 # 900 秒内至少 1 次写,则触发 BGSAVE |
BGSAVE 的关键机制是 fork + copy-on-write(写时复制):
1 | 1. 主进程 fork() 出一个子进程 |
这里的魔法是操作系统的 COW。fork 之后父子进程共享同一份物理内存页,只把页标记为只读;只有当主进程写某个页时,才真正复制那一页(缺页中断触发 copy)。于是子进程看到的始终是 fork 那一刻的一致快照,而主进程的后续写入不会污染它。这让 Redis 在不停服的情况下导出一致快照成为可能。
代价也藏在这里:写得越多,被复制的页越多,内存峰值越高。极端情况下(fork 期间几乎重写了所有数据),内存会接近翻倍。同时 fork 本身在大内存实例上不是免费的——它要复制页表,实例越大、页表越大,fork 越慢,这段时间主线程是被阻塞的。几十 GB 的实例,fork 可能造成几百毫秒甚至秒级的延迟尖刺。
RDB 的优点:文件紧凑、加载快(直接反序列化内存结构,无需逐条回放命令)、适合做备份和主从全量同步。缺点:两次快照之间的数据在崩溃时全部丢失——按上面的 save 配置,最坏可能丢几分钟数据。
AOF:把每条写命令追加成日志
AOF(Append Only File)换了个思路:记录每一条修改数据的写命令,重启时重新执行一遍就能还原状态。这就像数据库的 redo log,或者记账时不存余额而存每一笔流水。
写入路径不是直接落盘,而是经过操作系统缓冲:
1 | 命令执行 -> 追加到 aof_buf(Redis 内存缓冲) |
fsync 的时机由 appendfsync 控制,这是 AOF 最核心的可靠性/性能旋钮:
1 | appendfsync always # 每条命令都 fsync,最安全但最慢 |
always 几乎不丢数据,但每条写都要等磁盘,吞吐大跌;everysec 是工程上的甜点,在性能和安全间平衡,绝大多数生产用它。要注意 everysec 下,如果磁盘 IO 卡住导致上一次 fsync 没完成,Redis 主线程在某些情况下会被迫等待,引发延迟抖动——慢盘是 AOF 的隐性杀手。
AOF 重写:防止日志无限膨胀
只追加命令,文件会越来越大,而且充满冗余(对同一 key 反复 INCR 一百次,只需要最终值)。AOF 重写(rewrite) 解决这个问题:它不是读旧 AOF 压缩,而是直接遍历当前内存数据,生成"能重建当前状态的最小命令集"。同样用 fork 子进程做,不阻塞主线程。重写期间主进程新产生的写命令会暂存到缓冲区,重写完追加进去,保证不丢。
1 | auto-aof-rewrite-percentage 100 # 文件比上次重写后大了一倍就触发 |
混合持久化:取两者之长
新版 Redis 默认开启混合持久化(aof-use-rdb-preamble yes)。它在 AOF 重写时,把重写起点的全量数据以 RDB 二进制格式写在 AOF 文件头部,之后增量的写命令以 AOF 文本格式追加在后面:
1 | [ RDB 格式的全量快照 ][ 增量 AOF 命令 ... ] |
重启加载时,先快速反序列化 RDB 头(加载快),再回放尾部少量命令(数据新)。这样既有 RDB 的加载速度,又有 AOF 的低丢失窗口,是目前的推荐方案。
工程权衡:怎么选
- 能接受丢几分钟、要极致性能/最快重启:只开 RDB。常见于缓存场景(数据丢了能从源重建)。
- 要求数据尽量不丢:开 AOF +
everysec,配合混合持久化。 - 既要快速恢复又要低丢失:RDB + AOF 都开(默认混合),Redis 重启时优先用 AOF 恢复(因为它更新)。
容量/性能维度的核心制约都绕不开 fork 与内存峰值:无论 RDB 的 BGSAVE 还是 AOF 重写都要 fork,大实例上 fork 慢、COW 内存翻倍是共同的天花板。所以单实例内存不宜过大(通常建议控制在十几 GB 以内),太大就该分片。
常见踩坑
坑一:以为开了 AOF 就绝不丢数据。 everysec 仍有 1 秒窗口;write 进了 page cache 但还没 fsync 时断电,这部分也会丢。真要零丢失得用 always,并接受吞吐代价。
坑二:fork 时内存不足被 OOM。 COW 最坏要双倍内存,若机器内存只够装一份数据,BGSAVE/重写时会触发 OOM 或 fork 失败。要么留足内存,要么调小 vm.overcommit_memory(Linux 下需设为 1,否则 fork 可能直接失败)。
坑三:RDB 与 AOF 同开时混淆恢复来源。 两者都开时,重启只用 AOF 恢复(RDB 被忽略),因为 AOF 数据更新。有人以为加载了 RDB 导致排查方向错误。
坑四:磁盘满或慢导致主线程卡死。 AOF 的 fsync、RDB 的写盘都依赖磁盘,慢盘或盘满会让写操作阻塞,表现为 Redis 整体卡顿。监控磁盘 IO 和剩余空间是必要的。
小结
RDB 存"状态快照",紧凑、加载快、靠 fork+COW 实现不停服一致快照,但有较大丢失窗口且内存峰值高;AOF 存"操作流水",靠 appendfsync 在安全与性能间取舍、靠重写控制体积,丢失窗口小但文件大、加载慢。混合持久化用 RDB 头 + AOF 尾兼得二者之长,是现代 Redis 的默认与推荐配置。所有方案的共同硬约束是 fork 带来的延迟尖刺与内存翻倍,这也是单实例内存不能无限大的根本原因。