KernelRanch
返回文章列表

Redis 持久化三选一:重启后丢多少、恢复要多久

2026-11-10 #数据库

先说三个反直觉的实测结果:三种配置在 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 延迟
纯 RDB172,413.800.143 ms
AOF everysec194,174.750.127 ms
AOF always4,749.4710.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 万键耗时折算吞吐
纯 RDB384 ms520,833 /s
AOF everysec368 ms543,478 /s
AOF always4,045 ms49,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 万键落盘后:

配置文件构成总字节每键成本
纯 RDBdump.rdb5,176,956 (4.94 MiB)25.9 B
AOF everysec(未重写)base 89 + incr 19,088,918 + manifest 8819,093,191 (18.21 MiB)95.5 B
AOF everysec(重写后)base 5,176,956 + incr 0 + manifest 885,181,140 (4.94 MiB)25.9 B
AOF always(未重写)base 89 + incr 19,088,918 + manifest 8819,093,19195.5 B
AOF always(重写后)base 5,176,956 + incr 0 + manifest 885,181,14025.9 B

未重写的 AOF 是 RDB 的 3.69 倍。重写之后,AOF 的 base 文件字节数和 dump.rdb 完全相等——5,176,956,一个字节都不差。因为 Redis 7 的多部分 AOF(MP-AOF)把 base 文件直接写成 RDB 格式,重写等于给当前数据集拍了一张 RDB 快照,再挂一个空的自增文件。

重写耗时:

数据集重写耗时重写后体积
20 万键(everysec)187 ms5,181,140 B
20 万键(always)177 ms5,181,140 B
100 万键(everysec)691 ms26,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 自报加载
纯 RDB5,176,956297.4 / 318.8 / 344.4 ms318.8 ms0.132–0.138 s
AOF everysec 未重写19,093,191340.3 / 377.3 / 314.4 ms340.3 ms0.143–0.169 s
AOF everysec 重写后5,181,140277.5 / 393.4 / 346.1 ms346.1 ms0.121–0.131 s
AOF always 未重写19,093,191312.5 / 351.5 / 312.2 ms312.5 ms0.127–0.140 s
AOF always 重写后5,181,140309.9 / 333.7 ms321.8 ms0.127–0.128 s

五种状态的中位数落在 312 到 346 毫秒之间,最大间隔 34 毫秒。这个量级上,选哪种持久化对恢复速度没有可操作的影响。

把总耗时减去 Redis 自报的加载时间,剩下的 175 到 220 毫秒就是容器启动加 Redis 进程初始化的固定开销。这部分和持久化格式无关,却占了总恢复时间的一半以上。20 万键这个量级上,等的主要是容器起来。

100 万键时才拉开差距:

状态文件体积恢复耗时(3 次)中位Redis 自报加载
纯 RDB26,772,813995.9 / 1032.5 / 1000 ms1000 ms0.803–0.849 s
AOF everysec 未重写95,893,1921123.2 / 1128.3 / 1033 ms1123.2 ms0.849–0.956 s
AOF everysec 重写后26,776,999878.6 / 946.2 / 873 ms878.6 ms0.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,000200,0000 / 1000010,000
AOF everysec210,000210,00010,000 / 100000
AOF always210,000210,00010,000 / 100000

纯 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 '' + 手动 BGSAVE172,414 /s5,176,956 B中位 319 ms10,000
AOF everysec194,175 /s19,093,191 B,重写后 5,181,140 B中位 340 ms,重写后 346 ms0
AOF always4,749 /s19,093,191 B,重写后 5,181,140 B中位 313 ms,重写后 322 ms0

形状很清楚:吞吐上 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 固定配置

实测环境

环境规格用于
AWindows 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 字节与之完全相同,未重复测加载。