·约 11 分钟阅读·DeepSeek

Redis 持久化:RDB vs AOF,崩溃时你实际会丢失什么

一份带有真实基准测试的 Redis 持久化实战指南:RDB 快照、AOF 追加日志、appendfsync 策略、崩溃恢复与 AOF 重写——全部在 Docker 中实测。

devopsself-hosting

Redis 是一个内存数据库。每个 key 都存在于 RAM 中,这使它快——但也易失。进程停止的那一刻,数据全部消失。对于缓存来说没问题。对于主数据存储来说就是灾难。

本文将带你了解 Redis 提供的两种持久化机制——RDB 快照和 AOF(追加日志文件)——所有实验均在 Docker 中真实运行。我们将测量每种 appendfsync 策略的实际耐久窗口、做真实的崩溃恢复、观察 AOF 重写对文件大小的影响。读完后你将准确知道崩溃时会丢失什么,以及如何配置 Redis 使损失在可接受范围内。

如果你之前看过 Redis 模式 或 Redis vs Memcached,本文是自然的下一步:如何让 Redis 在重启后存活。

为什么内存存储需要持久化

Redis 将所有数据保存在内存中。读写直接命中 RAM,这就是你能看到亚毫秒级延迟的原因。但 RAM 是易失的——断电、kill -9 或内核 panic 都会清空一切。

持久化是将内存数据复制到磁盘以便重启后重新加载的机制。Redis 提供两种独立机制:

  • RDB(Redis 数据库备份): 某一时刻整个数据集的二进制快照,保存为 dump.rdb 文件。
  • AOF(追加日志文件): 每个写命令的日志,启动时重放以重建数据集。

你可以只用其中一个、两个都用、或两个都不用。两者都不是备份——后面会详细说明。

RDB:时间点快照

RDB 是数据集在某一时刻的二进制快照。用 BGSAVE(后台保存)或 SAVE(阻塞)触发。Redis fork 一个子进程将快照写入磁盘,父进程继续服务请求。

以下是在 Docker 容器中执行 RDB 保存的真实过程:

$ docker run --name redis-exp1 -d -p 6379:6379 redis:7-alpine
$ docker exec redis-exp1 sh -c 'redis-cli FLUSHALL && \
    redis-cli SET mykey "hello-rdb" && \
    redis-cli SET counter 42 && \
    redis-cli LPUSH mylist a b c d e && \
    redis-cli HSET user:1 name "Alice" age 30 && \
    redis-cli DBSIZE'
OK
OK
5
2
4

$ docker exec redis-aof sh -c 'redis-cli BGSAVE && sleep 1 && \
    redis-cli INFO persistence | grep -E "rdb_last_bgsave_status|rdb_last_save_time|rdb_changes_since_last_save" && \
    redis-cli LASTSAVE'
Background saving started
rdb_changes_since_last_save:0
rdb_last_save_time:1791858840
rdb_last_bgsave_status:ok
1791858840

$ docker exec redis-exp1 sh -c 'ls -la /data/dump.rdb'
-rw-------    1 redis    redis          191 Oct 13 02:34 /data/dump.rdb

4 个 key 的快照只有 191 字节。rdb_last_bgsave_status:ok 确认保存成功。LASTSAVE 返回上次成功保存的 Unix 时间戳。

RDB 你会丢失什么: 上次快照之后写入的所有数据。如果你每 5 分钟保存一次,在第 4 分钟崩溃,你就丢失了 4 分钟的写入。RDB 紧凑且加载快,但损失窗口很大——除非频繁保存,而频繁保存消耗 CPU 和磁盘 I/O。

AOF:追加日志文件

AOF 以 Redis 客户端通信相同的协议记录每个写命令。启动时 Redis 重放日志来重建数据集。AOF 在后台写入,你可以通过 appendfsync 控制日志刷新到磁盘的频率。

以下是一次写入后的 AOF 状态:

$ docker run --name redis-aof -d -p 6379:6379 redis:7-alpine \
    redis-server --appendonly yes --appendfsync always
$ docker exec redis-aof sh -c 'redis-cli SET testkey "aof-test" && \
    redis-cli INFO persistence | grep -E "aof_enabled|aof_last_write_status|aof_current_size|aof_base_size"'
OK
aof_enabled:1
aof_last_write_status:ok
aof_current_size:63
aof_base_size:0

$ docker exec redis-aof sh -c 'ls -la /data/appendonlydir/'
total 20
drwx------    2 redis    redis         4096 Oct 13 02:34 .
drwx------    3 redis    redis         4096 Oct 13 02:34 ..
-rw-------    1 redis    redis           89 Oct 13 02:34 appendonly.aof.1.base.rdb
-rw-------    1 redis    redis           63 Oct 13 02:34 appendonly.aof.1.incr.aof
-rw-------    1 redis    redis           88 Oct 13 02:34 appendonly.aof.manifest

AOF 以一个基础 RDB 文件开头(用于快速加载),以及一个随每次写入增长的增量 AOF 文件。

appendfsync:always vs everysec vs no

appendfsync 设置控制 AOF 从 OS 缓冲区缓存刷新到物理磁盘的频率。这是 Redis 中最重要的持久性旋钮。三种策略:

  • always — 每个写命令后 fsync。最高持久性,最低吞吐量。
  • everysec — 每秒 fsync 一次。崩溃时最多丢失 1 秒写入。默认值。
  • no — 从不主动 fsync;由 OS 决定何时刷新。最高吞吐量,最大丢失窗口。

我用 redis-benchmark 测量了每种策略的真实吞吐量成本(50,000 条 SET 命令,100 字节值):

# appendfsync always
$ docker exec redis-aof sh -c 'redis-benchmark -t set -n 50000 -q -d 100'
SET: 4114.21 requests per second, p50=4.439 msec

# appendfsync everysec
$ docker exec redis-aof sh -c 'redis-cli CONFIG SET appendfsync everysec && \
    redis-benchmark -t set -n 50000 -q -d 100'
SET: 69444.45 requests per second, p50=0.351 msec

# appendfsync no
$ docker exec redis-aof sh -c 'redis-cli CONFIG SET appendfsync no && \
    redis-benchmark -t set -n 50000 -q -d 100'
SET: 68399.45 requests per second, p50=0.351 msec

数字说明了一切:

策略吞吐量p50 延迟耐久窗口
always4,114 req/s4.4 ms零——每个写入都在磁盘上
everysec69,444 req/s0.35 ms≤ 1 秒
no68,399 req/s0.35 ms最长 30+ 秒(取决于 OS)

always 比 everysec 慢 17 倍。这就是每次写入都调用 fsync() 的代价。everysec 和 no 吞吐量几乎相同,因为两者都让 OS 缓冲写入——区别仅在于 OS 何时被强制刷新。

always 0 秒——每次写入都 fsync everysec ≤ 1 秒丢失窗口 no 最长 30+ 秒丢失窗口(OS 刷新) 崩溃 → 丢失窗口 = 上次 fsync 到崩溃之间的时间 在 Docker 中实测,redis:7-alpine,50k SET 操作
每种 appendfsync 策略的耐久窗口。always 不丢失任何数据;everysec 最多丢失 1 秒;no 丢失 OS 尚未刷新的所有数据。

真实崩溃恢复:你实际会丢失什么

理论很好,但 Redis 真正死掉时会发生什么?我通过写入数据、向 Redis 进程发送 SIGKILL、然后重启来测试。

测试 1:appendfsync everysec

$ docker exec redis-aof sh -c 'redis-cli CONFIG SET appendfsync everysec && \
    redis-cli FLUSHALL && \
    redis-cli SET crash_test "before-crash-everysec" && \
    redis-cli SET counter 1 && \
    redis-cli DBSIZE'
OK
OK
OK
OK
2

$ docker kill -s 9 redis-aof && sleep 1 && docker start redis-aof && sleep 2
$ docker exec redis-aof sh -c 'redis-cli PING && \
    redis-cli GET crash_test && \
    redis-cli GET counter && \
    redis-cli DBSIZE'
PONG
before-crash-everysec
1
2

所有数据都恢复了。使用 everysec 时,最坏情况是丢失最后一秒的写入——但在这个测试中,写入发生在崩溃前超过一秒,所以所有数据都已经 fsync 到磁盘。

测试 2:appendfsync no

$ docker exec redis-aof sh -c 'redis-cli CONFIG SET appendfsync no && \
    redis-cli FLUSHALL && \
    redis-cli SET crash_test "before-crash-no" && \
    redis-cli SET counter 99 && \
    redis-cli DBSIZE'
OK
OK
OK
OK
2

$ docker kill -s 9 redis-aof && sleep 1 && docker start redis-aof && sleep 2
$ docker exec redis-aof sh -c 'redis-cli PING && \
    redis-cli GET crash_test && \
    redis-cli GET counter && \
    redis-cli DBSIZE'
PONG
before-crash-no
99
2

意外吗?其实不是。kill -9 杀死 Redis 进程,但 OS 缓冲区缓存还在——数据仍在内存中,只是尚未写入物理磁盘。Redis 重启时读取 AOF 文件,OS 从页缓存提供数据。appendfsync no 的数据丢失只在 OS 本身崩溃或机器在缓冲区缓存刷新前断电时才会发生。

这就是事实:appendfsync no 在 Redis 进程崩溃时不会丢失数据。它在 OS 崩溃时丢失数据。丢失窗口是 OS 决定等待刷新的时间——Linux 上通常 30 秒,但在内存压力下可能更长。

AOF 重写:压缩日志

AOF 随写入不断增长。一个 key 更新 1,000 次,日志中就出现 1,000 次。Redis 通过 BGREWRITEAOF 解决这个问题——fork 一个子进程,写入只包含每个 key 当前状态的最小 AOF。

我写入 100 个带长值的 key,然后触发重写:

$ docker exec redis-aof sh -c 'redis-cli CONFIG SET appendfsync no && \
    redis-cli FLUSHALL && \
    for i in $(seq 1 100); do redis-cli SET key:$i "value-$i-padding-padding-padding" > /dev/null; done && \
    redis-cli DBSIZE && \
    ls -la /data/appendonlydir/'
OK
OK
100
-rw-------    1 redis    redis     23856834 Oct 13 02:36 appendonly.aof.1.incr.aof

$ docker exec redis-aof sh -c 'redis-cli BGREWRITEAOF && sleep 2 && \
    ls -la /data/appendonlydir/ && \
    redis-cli DBSIZE'
Background append only file rewriting started
-rw-------    1 redis    redis          3479 Oct 13 02:36 appendonly.aof.2.base.rdb
-rw-------    1 redis    redis            0 Oct 13 02:36 appendonly.aof.2.incr.aof
100

AOF 从 23.8 MB 降到 3.5 KB——减少了 99.98%。重写用紧凑的基础 RDB 文件替换了臃肿的增量 AOF。全部 100 个 key 完好无损。

AOF 重写在 AOF 超过 auto-aof-rewrite-percentage(默认 100%)且超过 auto-aof-rewrite-min-size(默认 64mb)时自动触发。你也可以用 BGREWRITEAOF 手动触发。

同时运行两者:推荐的默认配置

推荐的生产配置是 RDB + AOF 同时启用。原因如下:

  • AOF 给你较小的耐久窗口(使用 everysec 时 ≤ 1 秒)。
  • RDB 给你紧凑、快速加载的快照,用于灾难恢复和快速重启。

两者都启用时,Redis 启动时优先加载 AOF(因为更新),AOF 缺失或损坏时回退到 RDB。如果 AOF 增长过大,RDB 也可作为快速恢复点。

以下是两者都启用时 INFO persistence 的输出:

$ docker exec redis-aof sh -c 'redis-cli INFO persistence | grep -E "rdb_|aof_" | head -30'
rdb_changes_since_last_save:100
rdb_bgsave_in_progress:0
rdb_last_save_time:1791859010
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:-1
rdb_current_bgsave_time_sec:-1
rdb_saves:0
rdb_last_cow_size:0
rdb_last_load_keys_expired:0
rdb_last_load_keys_loaded:0
aof_enabled:1
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:0
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_rewrites:1
aof_rewrites_consecutive_failures:0
aof_last_write_status:ok
aof_last_cow_size:819200
aof_current_size:3479
aof_base_size:3479
aof_pending_rewrite:0
aof_buffer_length:0
aof_pending_bio_fsync:0
aof_delayed_fsync:0

rdb_changes_since_last_save:100 告诉你自上次 RDB 快照以来发生了多少次写入。aof_rewrites:1 确认 AOF 重写成功执行。

事实真相

让我直接说明 Redis 持久化是什么、不是什么:

  1. Redis 在未配置持久化时就是缓存。 默认情况下,Redis 按间隔保存 RDB 快照(默认:1+ key 变化时 5 分钟,10+ key 变化时 1 分钟)。如果你从不修改配置,你运行的就是一个 5 分钟丢失窗口的缓存。

  2. 复制不是备份。 副本是数据的实时副本。如果你在主节点执行 FLUSHALL,副本也会执行。复制防止硬件故障,不防止逻辑错误。

  3. 丢失窗口是真实的。 即使使用 appendfsync everysec,崩溃时也会丢失最多 1 秒写入。使用 no 时丢失最多 30+ 秒。仅使用 RDB 时,丢失上次快照以来的所有数据。

  4. 备份是独立的。 如果需要时间点恢复,将 dump.rdb 或 AOF 文件复制到服务器外存储。Redis 持久化是为了在重启后存活,不是为了从误操作 DEL 中恢复。

决策建议

以下是我的实用建议:

  • 纯缓存: 使用默认间隔的 RDB,或完全不用持久化。重启时丢失数据没关系——缓存会重建。
  • 主数据存储,可丢失 1 秒: AOF 使用 appendfsync everysec + RDB 用于快速重启。这是大多数应用的最佳平衡点。
  • 主数据存储,不能丢失任何数据: AOF 使用 appendfsync always。接受 17 倍吞吐量损失。实践中,大多数人使用 everysec 并接受 1 秒窗口。
  • 最大吞吐量,可丢失数据: AOF 使用 appendfsync no,或仅用 RDB。适合可重建的临时数据存储如会话。

配置持久化,测量你的实际丢失窗口,对你能承受的丢失做出清醒的决定。这就是工作。


实验在 Docker 中的 redis:7-alpine 上运行。所有数字均为容器中的真实测量值。测试后已清理。