Redis 持久化三选一:重启后丢多少、恢复要多久
先说三个反直觉的实测结果:三种配置在 20 万键规模下,重启恢复耗时都在 300 毫秒上下,彼此差不到 30 毫秒;AOF 文件比 RDB 大 3.69 倍,但加载时间只多 20 毫秒左右;真正把三者区分开的是「崩溃丢多少」,纯 RDB 丢了尾部整整 10000 条,而两个 AOF 配置一条没丢——包括那个理论上每秒最多丢 1 秒数据的 everysec。至于写入吞吐,always 比 everysec 慢 41 倍,可这个 41 倍里有一大半是客户端造成的,换成 pipeline 批量写就只剩 10 倍。下面把四组数字逐个摊开,说明每个数字是在什么条件下测出来的。
1. 怎么测的
环境是 Windows 11 + Docker Desktop 29.4.3(WSL2 后端),镜像 redis:7,实际版本 Redis 7.4.12。
| 项目 | 配置 |
|---|---|
| 宿主 | Windows 11, Docker Desktop 29.4.3, WSL2 |
| 镜像 / 版本 | redis:7 / Redis 7.4.12, jemalloc-5.3.0 |
| 容器内存上限 | --memory 512m |
| 客户端 | 容器内 redis-cli / redis-benchmark,经 docker exec 调用 |
| 数据集 | 20 万键,k:<n> → v:<60 位补零>,每键约 75 字节 |
| 实测内存占用 | 20 万键 38.10 MB;100 万键 184.46 MB |
没有做 -p 端口映射。Windows 的动态保留端口段会和 Docker 抢端口,这一带经常直接 bind 失败,所以客户端全跑在容器内部,走 docker exec。代价是每次 exec 自带几十毫秒开销,凡涉及「毫秒级」的结论都另用容器内时钟校过一次。
三种配置都是靠启动参数固定的,避免 CONFIG SET 在重启后失效:
# A 纯 RDB:关掉自动快照,只在需要时手动 BGSAVE
docker run -d --name r-rdb --memory 512m redis:7 \
redis-server --save '' --appendonly no
# B AOF everysec
docker run -d --name r-everysec --memory 512m redis:7 \
redis-server --save '' --appendonly yes --appendfsync everysec
# C AOF always
docker run -d --name r-always --memory 512m redis:7 \
redis-server --save '' --appendonly yes --appendfsync always
B 和 C 也带 --save '',是为了把 RDB 这个变量摘掉,只留 AOF 的 fsync 策略在变。A 组用「关自动快照 + 手动 BGSAVE 一次」来代表 RDB 场景,这也正是很多团队的真实用法:怕 fork 抖动,把 save 清空,靠定时任务或运维动作补快照。
写入用 redis-cli --pipe 灌入,命令行本身可复现:
seq 1 200000 | awk '{printf "SET k:%d v:%060d\n", $1, $1}' | redis-cli --pipe
丢数据那组测试需要「写入正在发生时崩掉」,所以另写了一个限速写入器,把 10000 条尾部数据摊到 9 秒多写完(约 1000 条/秒),写完立刻 docker kill:
TOTAL=$1; PFX=$2; BATCH=$3; SL=$4
i=1
while [ "$i" -le "$TOTAL" ]; do
end=$((i+BATCH-1))
seq "$i" "$end" | awk -v p="$PFX" '{printf "SET %s%d v:%060d\n", p, $1, $1}' \
| redis-cli --pipe > /dev/null
i=$((end+1)); sleep "$SL"
done
限速而非全速灌,是为了让「1 秒窗口」这个说法可被检验。全速灌 10000 条只用 20 毫秒,落在哪个 fsync 间隙全看运气,测出来的是噪声。
2. 写入吞吐:41 倍差距里有一大半是客户端造成的
非 pipeline 的逐条 SET,50 并发,redis-benchmark -n 20000 -c 50 -t set -d 128:
| 配置 | 吞吐 (requests/s) | p50 延迟 |
|---|---|---|
| 纯 RDB | 172,413.80 | 0.143 ms |
| AOF everysec | 194,174.75 | 0.127 ms |
| AOF always | 4,749.47 | 10.431 ms |
always 相对 everysec 是 40.9 倍差距,p50 延迟差 82 倍。RDB 和 everysec 几乎贴在一起(172k 与 194k),因为两者都不在写路径上等磁盘:RDB 只在 fork 那一刻受影响,everysec 的 fsync 丢给后台线程。
always 的 10.4 毫秒 p50 基本就是这台机器上一次 fsync 的代价。WSL2 overlay 上的 fsync 不便宜,比裸金属 NVMe 的常见值差一个量级,4749 rps 是「这台机器」的数字,不是 Redis 的上限。
但同一份 20 万键,换成 pipeline 批量灌入,always 的表现完全不同:
| 配置 | 20 万键耗时 | 折算吞吐 |
|---|---|---|
| 纯 RDB | 384 ms | 520,833 /s |
| AOF everysec | 368 ms | 543,478 /s |
| AOF always | 4,045 ms | 49,444 /s |
always 从 4749 /s 跳到 49,444 /s,快了 10.4 倍。原因是组提交:管道里的一批命令只触发一次 write 加一次 fsync,而不是每条一次。官方 persistence 文档对 always 的描述就写明「命令是在多个客户端或一个 pipeline 的一批命令执行完之后才追加到 AOF 的,也就是一次 write 和一次 fsync」。
这解释了为什么压测报告里 always 的数字能从 4000 一路报到 50000:两边都没造假,只是客户端批量不同。比较 always 的吞吐时,不写清并发模型和是否 pipeline,那个数字没有意义。
3. 文件体积:AOF 是 RDB 的 3.69 倍,重写后一分不差
20 万键落盘后:
| 配置 | 文件构成 | 总字节 | 每键成本 |
|---|---|---|---|
| 纯 RDB | dump.rdb | 5,176,956 (4.94 MiB) | 25.9 B |
| AOF everysec(未重写) | base 89 + incr 19,088,918 + manifest 88 | 19,093,191 (18.21 MiB) | 95.5 B |
| AOF everysec(重写后) | base 5,176,956 + incr 0 + manifest 88 | 5,181,140 (4.94 MiB) | 25.9 B |
| AOF always(未重写) | base 89 + incr 19,088,918 + manifest 88 | 19,093,191 | 95.5 B |
| AOF always(重写后) | base 5,176,956 + incr 0 + manifest 88 | 5,181,140 | 25.9 B |
未重写的 AOF 是 RDB 的 3.69 倍。重写之后,AOF 的 base 文件字节数和 dump.rdb 完全相等——5,176,956,一个字节都不差。因为 Redis 7 的多部分 AOF(MP-AOF)把 base 文件直接写成 RDB 格式,重写等于给当前数据集拍了一张 RDB 快照,再挂一个空的自增文件。
重写耗时:
| 数据集 | 重写耗时 | 重写后体积 |
|---|---|---|
| 20 万键(everysec) | 187 ms | 5,181,140 B |
| 20 万键(always) | 177 ms | 5,181,140 B |
| 100 万键(everysec) | 691 ms | 26,776,999 B |
100 万键时:RDB 26,772,813 字节(25.5 MiB,每键 26.77 字节),未重写 AOF 95,893,192 字节(91.5 MiB,每键 95.9 字节),后者是前者的 3.58 倍。每键成本几乎不随规模变化,这个倍数可以外推。
磁盘预算因此要按两套算。AOF 的稳态体积取决于上次重写到现在的写入量,而非数据集大小,回落点由 auto-aof-rewrite-percentage 与 auto-aof-rewrite-min-size 决定。容量规划用「数据集 × 3.7」当上界,比按 RDB 体积估更安全。
4. 恢复耗时:20 万键时三者没区别,100 万键时才有
恢复时间的定义是「容器 StartedAt 到第一个真正返回 PONG 的时刻」。这里踩过一个坑,值得写出来。
第一版探针写的是 until redis-cli PING; do sleep; done,测出来 100 万键只要 358 毫秒,比 20 万键还快。查下去发现:Redis 在加载数据集时会接受连接,并对命令回 -LOADING 错误,而 redis-cli 收到错误回复时退出码仍然是 0。于是循环在「连接得上但还没加载完」时就退出了,测到的是「端口通了」而不是「能服务」。改成字符串比对就正常了:
until [ "$(redis-cli PING 2>/dev/null)" = "PONG" ]; do sleep 0.005; done
date +%s%N
容器和宿主共享内核时钟,所以容器内 date +%s%N 可以直接和宿主侧 docker inspect 的 StartedAt 相减,避开了每次 docker exec 几十毫秒的启动开销。
20 万键,每种状态重启 3 次:
| 状态 | 文件体积 | 恢复耗时(3 次) | 中位 | Redis 自报加载 |
|---|---|---|---|---|
| 纯 RDB | 5,176,956 | 297.4 / 318.8 / 344.4 ms | 318.8 ms | 0.132–0.138 s |
| AOF everysec 未重写 | 19,093,191 | 340.3 / 377.3 / 314.4 ms | 340.3 ms | 0.143–0.169 s |
| AOF everysec 重写后 | 5,181,140 | 277.5 / 393.4 / 346.1 ms | 346.1 ms | 0.121–0.131 s |
| AOF always 未重写 | 19,093,191 | 312.5 / 351.5 / 312.2 ms | 312.5 ms | 0.127–0.140 s |
| AOF always 重写后 | 5,181,140 | 309.9 / 333.7 ms | 321.8 ms | 0.127–0.128 s |
五种状态的中位数落在 312 到 346 毫秒之间,最大间隔 34 毫秒。这个量级上,选哪种持久化对恢复速度没有可操作的影响。
把总耗时减去 Redis 自报的加载时间,剩下的 175 到 220 毫秒就是容器启动加 Redis 进程初始化的固定开销。这部分和持久化格式无关,却占了总恢复时间的一半以上。20 万键这个量级上,等的主要是容器起来。
100 万键时才拉开差距:
| 状态 | 文件体积 | 恢复耗时(3 次) | 中位 | Redis 自报加载 |
|---|---|---|---|---|
| 纯 RDB | 26,772,813 | 995.9 / 1032.5 / 1000 ms | 1000 ms | 0.803–0.849 s |
| AOF everysec 未重写 | 95,893,192 | 1123.2 / 1128.3 / 1033 ms | 1123.2 ms | 0.849–0.956 s |
| AOF everysec 重写后 | 26,776,999 | 878.6 / 946.2 / 873 ms | 878.6 ms | 0.677–0.721 s |
未重写的 AOF 比 RDB 多花约 120 毫秒,加载时间 0.85–0.96 秒对 0.80–0.85 秒。体积大 3.58 倍,加载只慢 10%,因为 AOF 加载是顺序重放命令、纯内存操作,而 RDB 要重建哈希表。
最后一行的数字需要打个问号:重写后的 AOF(26,776,999 字节)和 RDB(26,772,813 字节)体积几乎相同,加载时间却是 0.677–0.721 秒对 0.803–0.849 秒,快了 120 毫秒。同一份字节量、同一种格式(base 就是 RDB),不该有这种差距。更可能的解释是页缓存温度:AOF 那组刚做完重写,26 MB 的 base 刚被写出、还在宿主缓存里,而 dump.rdb 是更早写的。这一格不该当成「重写后加载更快」的证据,只能说明两者同一量级。
5. 丢多少:只有 RDB 真的丢了
这组是全文唯一有明确胜负的维度。做法:先写入并持久化 20 万键的基线,再以约 1000 条/秒灌入 10000 条 t: 前缀的尾部数据,写完立刻 docker kill(SIGKILL),然后 docker start,数 t: 键还剩多少。
| 配置 | 崩溃前 DBSIZE | 崩溃后 DBSIZE | 尾部存活 | 丢失 |
|---|---|---|---|---|
| 纯 RDB(手动 BGSAVE 过一次) | 200,000 | 200,000 | 0 / 10000 | 10,000 |
| AOF everysec | 210,000 | 210,000 | 10,000 / 10000 | 0 |
| AOF always | 210,000 | 210,000 | 10,000 / 10000 | 0 |
纯 RDB 把 10000 条尾部全丢了,基线 20 万条完好——因为最后一次 BGSAVE 之后再没落过盘。这就是 save '' 加手动快照的真实代价:丢失量等于「上次快照到崩溃之间的全部写入」,没有上界,只有快照频率决定的分母。
everysec 和 always 都是 0 丢失。everysec 丢 0 这个结果,和「每秒最多丢 1 秒数据」的通行说法对不上,所以去翻了崩溃瞬间的 AOF 文件。在 docker kill 之后、docker start 之前,把容器的 appendonlydir 整个拷出来数:
appendonly.aof.3.base.rdb 5,176,956 B
appendonly.aof.3.incr.aof 1,877,834 B
appendonly.aof.manifest 88 B
incr 文件中的 SET 命令数 = 20,000(含上一轮同类测试的 10,000 条)
文件末尾 = ...v:000000000000000000000000000000000000000000000000000000010000\r\n
是否以完整命令结尾 = True
20000 条 SET 命令全在文件里,文件以一条完整命令的 \r\n 结尾,没有任何截断。也就是说,在 SIGKILL 落下的时候,这 10000 条写入早就通过 write 进了文件——它们待在操作系统的页缓存里,只是还没 fsync 到盘。
SIGKILL 杀的是进程,不是内核。页缓存里的脏页属于内核,进程死了照样会被回写。所以「everysec 每秒最多丢 1 秒」这个窗口,在进程级崩溃里根本不会打开;它要生效,得让页缓存一起消失——整机断电、内核 panic、虚拟机被强行 reset,或者宿主机存储掉电。
这条结论有实用价值:故障模型是「进程 OOM 被杀」「容器被重启」「Pod 被驱逐」时,everysec 和 always 在数据安全性上没有区别,都是 0 丢失。只有故障模型包含断电或存储失联,always 的那次 fsync 才买到它该买的东西,代价是第 2 节那 41 倍(pipeline 下 10 倍)的吞吐。
always 同样丢 0,符合预期。它和 everysec 的差别不在「进程崩溃丢不丢」,而在「断电丢不丢」。
这套数字的边界要写清楚:这里只做了 docker kill(SIGKILL),验证的是进程级崩溃。整机断电、虚拟机被 reset、宿主内存回收把页缓存一起丢掉,这三种场景都没测,那里 everysec 的 1 秒窗口依然存在。
6. 四个数字放一起
| 配置 | 写入吞吐(非 pipeline) | 持久化体积(20 万键) | 重启恢复(20 万键) | 崩溃丢失(10000 条尾部) |
|---|---|---|---|---|
纯 RDB, save '' + 手动 BGSAVE | 172,414 /s | 5,176,956 B | 中位 319 ms | 10,000 |
| AOF everysec | 194,175 /s | 19,093,191 B,重写后 5,181,140 B | 中位 340 ms,重写后 346 ms | 0 |
| AOF always | 4,749 /s | 19,093,191 B,重写后 5,181,140 B | 中位 313 ms,重写后 322 ms | 0 |
形状很清楚:吞吐上 RDB 与 everysec 并列第一、always 掉一个数量级;体积上 RDB 与重写后的 AOF 并列;恢复耗时三者拉不开;丢失量只有 RDB 归零失败。
7. 怎么选
故障模型只到进程级,又要吞吐,everysec 是默认答案:和 RDB 同档的写入性能,加上进程崩溃 0 丢失。前提是别让 AOF 无限膨胀——重写后的 AOF 和 RDB 一样大,恢复时间也回到同一水平。
故障模型包含断电,或者数据重要性要求「已确认的写就不能丢」,才轮到 always。上 always 之前先确认客户端是批量写还是逐条写,逐条写的 always 在这台机器上只有 4749 /s。
纯 RDB 适合能接受「丢失量等于快照间隔内全部写入」的场景。风险不在平均值而在上界:save '' 加手动快照意味着丢失量由运维动作的频率决定,忘了跑就是全丢。只用 RDB 的话,至少留一组 save 规则兜底。
生产上更常见的是两者都开:Redis 7 重启时会优先用 AOF 重建,RDB 留作备份。
小结
四组数字里,只有「丢多少」真正区分了三种配置,而它的答案还依赖一个常被忽略的前提:进程崩溃和整机断电是两个不同的故障模型。SIGKILL 打不掉页缓存,所以 everysec 在容器重启、OOM、Pod 驱逐这类场景下和 always 一样是 0 丢失;那 1 秒窗口要在断电、内核 panic 或虚拟机 reset 时才打开。选型时先写清故障模型,再谈 fsync 策略,否则就是在为不存在的风险付吞吐。
体积和恢复这两列容易被高估。AOF 未重写时确实是 RDB 的 3.69 倍,但重写后字节数完全相同,恢复耗时在 20 万键和 100 万键两个量级上都只差 10% 上下,而容器启动那 175 到 220 毫秒的固定开销谁都得付。把「AOF 恢复慢」当成选 RDB 的理由,在百万键以内站不住。
真正需要盯住的是快照策略和重写策略,而不是在三种模式之间反复横跳。纯 RDB 的丢失量没有上界,只有快照频率决定的分母;AOF 的磁盘占用没有稳态上界,只有 auto-aof-rewrite-percentage 决定的回落点。
数据来源与检索时间
文档检索时间 2026-11-10;全部实测数字来自 2026-11-10 本机 Docker 环境,同一批次采集。
| 编号 | 来源 | URL | 用于 |
|---|---|---|---|
| [1] | Redis 官方文档《Redis persistence》(RDB/AOF 优缺点、appendfsync 三档语义、MP-AOF 与重写机制) | https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/ | §1 配置语义、§3 MP-AOF base 为 RDB 格式、§5 everysec 1 秒窗口的定义 |
| [2] | Redis 官方文档《BGSAVE》(fork 后父进程继续服务、与 AOF 重写互斥) | https://redis.io/docs/latest/commands/bgsave/ | §2 RDB 写入路径不受磁盘阻塞、§3 手动快照的语义 |
| [3] | Redis 官方文档《CONFIG SET》(运行时切换持久化、appendonly 的等效性) | https://redis.io/docs/latest/commands/config-set/ | §1 为什么用启动参数而非 CONFIG SET 固定配置 |
实测环境
| 环境 | 规格 | 用于 |
|---|---|---|
| A | Windows 11 + Docker Desktop 29.4.3(WSL2 后端),Redis 7.4.12,容器 --memory 512m | 全部四组数字:写入吞吐、文件体积、重启恢复、崩溃丢失,以及 100 万键的扩展对照 |
核验说明
- 所有体积均为容器内
ls -l/du -sb读出的字节数,未做换算后回填;MiB 字样旁均标注了原始字节。 - 吞吐数字取
redis-benchmark -n 20000 -c 50 -t set -d 128的输出原值;pipeline 数字取redis-cli --pipe前后容器内date +%s%N的差值。 - 重启恢复时间每组重启 3 次并给出全部三次原始值与中位数;测量口径是容器
StartedAt到容器内首次返回PONG,用共享内核时钟对齐,不含docker exec启动开销。 - 第一版恢复探针有缺陷(
redis-cli在-LOADING错误回复下退出码仍为 0,加载未完成就提前返回),100 万键因此被低估到 358 毫秒。发现后全部重测,文中数字来自修正后的探针,缺陷记在 §4。 - 崩溃丢失测试用
docker kill(SIGKILL)而非docker restart。docker restart走 SIGTERM,Redis 会正常收尾,测不出崩溃语义。限速写入器的实际速率为 1006–1080 条/秒(10000 条耗时 9.26–9.95 秒)。 - §5 关于页缓存的判断基于崩溃瞬间拷出的 AOF 文件内容(20000 条完整 SET 命令、以完整命令结尾),不是推断。未做整机断电复现,因此「断电时 everysec 会丢约 1 秒」仍引自官方文档 [1],不是本机实测。
- 未做的事:没测
appendfsync no;没在物理机 + 独立 NVMe 上复测,4,749 rps 与 10.4 毫秒 p50 是本机 WSL2 overlay 的数字,不能当作裸金属表现;100 万键只测 everysec,always 的 AOF 字节与之完全相同,未重复测加载。