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 延迟 | 耐久窗口 |
|---|---|---|---|
always | 4,114 req/s | 4.4 ms | 零——每个写入都在磁盘上 |
everysec | 69,444 req/s | 0.35 ms | ≤ 1 秒 |
no | 68,399 req/s | 0.35 ms | 最长 30+ 秒(取决于 OS) |
always 比 everysec 慢 17 倍。这就是每次写入都调用 fsync() 的代价。everysec 和 no 吞吐量几乎相同,因为两者都让 OS 缓冲写入——区别仅在于 OS 何时被强制刷新。
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 持久化是什么、不是什么:
-
Redis 在未配置持久化时就是缓存。 默认情况下,Redis 按间隔保存 RDB 快照(默认:1+ key 变化时 5 分钟,10+ key 变化时 1 分钟)。如果你从不修改配置,你运行的就是一个 5 分钟丢失窗口的缓存。
-
复制不是备份。 副本是数据的实时副本。如果你在主节点执行
FLUSHALL,副本也会执行。复制防止硬件故障,不防止逻辑错误。 -
丢失窗口是真实的。 即使使用
appendfsync everysec,崩溃时也会丢失最多 1 秒写入。使用no时丢失最多 30+ 秒。仅使用 RDB 时,丢失上次快照以来的所有数据。 -
备份是独立的。 如果需要时间点恢复,将
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 上运行。所有数字均为容器中的真实测量值。测试后已清理。