重启后数据没了,第一反应是"Redis 不是会存盘吗"。它确实会,但有前提:配了哪种方式、写到什么程度、磁盘与内存够不够。默认配置下只在满足条件时落一次快照,最后一次快照之后的写入,进程一停就归零。
两件事要拆开看:数据怎么落盘决定单机丢多少,副本怎么同步决定故障时能救回多少。
1. 先分清两种"没了"
| 现象 | 真实原因 | 检查入口 |
|---|---|---|
| 重启后全部数据消失 | 没开持久化,或 dir 指到了临时目录 |
CONFIG GET dir、CONFIG GET save |
| 重启后少了最近几分钟 | RDB 间隔太长 / AOF 未开 | CONFIG GET appendonly、INFO persistence |
| 重启后文件在但加载失败 | RDB 损坏、AOF 尾部截断 | redis-check-rdb、redis-check-aof |
| 主从切换后回退到旧数据 | 从库数据旧、切换前未校验 offset | INFO replication 的 master_repl_offset |
2. RDB:三种触发方式,一个 fork 代价
| 触发 | 方式 | 典型场景 |
|---|---|---|
| 规则触发 | save 900 1、save 300 10、save 60 10000 |
日常自动快照 |
| 手动 | BGSAVE(后台)/ SAVE(阻塞,别在生产用) |
备份前、变更前 |
| 复制触发 | 从库首次全量同步时主库自动 BGSAVE |
加副本、副本重建 |
# redis.conf 关键项(按 7.x 核对)
save 900 1
save 300 10
save 60 10000
dbfilename dump.rdb
dir /var/lib/redis
rdbcompression yes
stop-writes-on-bgsave-error yes # 落盘失败是否拒写,生产建议保持 yes
BGSAVE 靠 fork() 出子进程写盘,父进程继续服务。代价不在磁盘,在 fork 本身:fork 要复制页表,内存越大越慢;子进程写盘期间父进程的写入触发写时复制(COW),写入越密集,额外内存占用越高。典型量级是:内存越大、写入越频,抖动越明显,具体毫秒数按你的机器实测。
3. AOF:三种 appendfsync 的丢失窗口
| 策略 | 行为 | 最多丢 | 代价 |
|---|---|---|---|
always |
每条命令都 fsync | 几乎不丢 | 吞吐骤降,通常不用于高写入场景 |
everysec |
每秒 fsync 一次(默认) | 约 1 秒写入 | 兼顾性能与安全,绝大多数场景的默认选择 |
no |
交给 OS 决定刷盘 | 可达数十秒 | 最快,也最不可控 |
appendonly yes
appendfsync everysec
appendfilename "appendonly.aof"
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-use-rdb-preamble yes # 混合持久化,7.x 默认
注意 Redis 7.x 的 AOF 是多文件结构(appendonlydir/ 下 base + incr 文件),不再是一个 appendonly.aof。改目录结构后,旧的备份脚本会拷错文件——这是升级到 7.x 后最常见的备份失效原因。
4. AOF 重写:文件为什么不会无限涨
AOF 记的是命令流水,INCR 一万次就是一万条。重写做的事是用当前数据状态生成一份最小命令集,把一万条 INCR 压成一条 SET。
| 项 | 说明 |
|---|---|
| 触发条件 | 增长率 ≥ auto-aof-rewrite-percentage 且 ≥ auto-aof-rewrite-min-size |
| 谁执行 | 子进程重写,父进程把新写入同时追加到旧 AOF 与重写缓冲 |
| 额外开销 | fork 一次 + 重写期间双份写入 + 内存中的重写缓冲 |
| 风险点 | 磁盘空间要能同时容纳旧文件与新文件(峰值近 2 倍) |
重写期间如果磁盘被写满,stop-writes-on-bgsave-error 会让 Redis 直接拒写——表现为"服务还活着,但所有写命令报错"。
5. 混合持久化:为什么默认这么配
| 方案 | 恢复速度 | 丢数据窗口 | 文件可读性 |
|---|---|---|---|
| 纯 RDB | 快 | 一个快照间隔 | 二进制,需工具读 |
| 纯 AOF | 慢(重放全部命令) | 约 1 秒 | 文本,可人工检查 |
| 混合(RDB 头 + AOF 尾) | 快 | 约 1 秒 | 半二进制,加载最快 |
取舍很清楚:用 RDB 的加载速度,换 AOF 的丢数据窗口。它把"重启等几分钟"变成"几秒",代价是文件不再是纯文本。除非有审计需求,默认开即可。
6. maxmemory 与持久化:内存满了也可能救不回
| 情况 | 结果 |
|---|---|
小于数据量 + noeviction |
写入报 OOM,已有数据完好,落盘正常 |
淘汰策略(allkeys-lru 等) |
旧 key 被驱逐,落盘的是被驱逐后的数据集 |
| 设得过大,接近物理内存 | BGSAVE 的 COW 峰值可能触发 OOM Killer |
落盘失败 + stop-writes-on-bgsave-error yes |
拒写,业务看到"连上了但不能写" |
第二条最易被误判成"持久化丢数据":其实是淘汰策略先删了。对比 INFO stats 的 evicted_keys 与 INFO keyspace 的 key 数即可确认。
实用规则:至少留一份数据集大小的空闲内存给 fork 与 COW,否则快照本身成为故障源。
7. 复制的三个阶段
| 阶段 | 做什么 | 关键参数 |
|---|---|---|
| 全量同步 | 主库 BGSAVE 出 RDB,传给从库加载 |
repl-diskless-sync、repl-diskless-sync-delay |
| 增量同步 | 从库带上 offset,主库从 backlog 补发缺失部分 |
repl-backlog-size(默认 1mb) |
| 命令传播 | 之后主库每条写命令实时发给从库 | 无(持续过程) |
repl_backlog 是一个环形缓冲。从库断线时间 × 写入速率 > backlog 大小时,补发失败,退化成全量同步——大实例上这等于一次网络与磁盘的双重冲击。
repl-backlog-size 64mb # 按"最长可容忍断线时间 × 写入速率"估算
repl-diskless-sync yes # 无盘复制,省一次落盘
repl-diskless-sync-delay 5
min-replicas-to-write 1 # 至少 1 个副本确认才允许写,防脑裂丢数据
min-replicas-max-lag 10
后两项是用可用性换一致性:副本失联超过 10 秒,主库自动拒写。小集群里很多人不开,代价是切换时可能丢一段写入。
8. 复制延迟与读写分离的一致性风险
$ redis-cli -p 6380 INFO replication
role:slave
master_link_status:up
master_last_io_seconds_ago:0
slave_repl_offset:184320
master_repl_offset:184512 # 差 192,说明有积压
| 风险 | 表现 | 缓解 |
|---|---|---|
| 读到自己刚写的数据失败 | 写完立刻查从库,查不到 | 关键读走主库,或写后延迟读 |
| 延迟随写入峰值放大 | 高峰期从库落后明显 | 监控两个 offset 差值并告警 |
| 从库阻塞导致积压 | 慢命令、落盘卡顿 | 关从库的 appendonly,减轻压力 |
| 全量同步反复触发 | 抖动 + backlog 太小 | 加大 backlog |
读写分离不是免费的:一致性从"Redis 保证"变成"你的代码保证"。凡有"写完马上读"语义的路径,默认走主库。
9. 故障切换的三个层次与脑裂
| 层次 | 谁发现故障 | 谁做切换 | 风险 |
|---|---|---|---|
| 人工 | 人 | 人改配置/改 DNS | 慢,但可控;适合小规模 |
| 哨兵 | Sentinel 集群投票 | Sentinel 提升从库 | 主观下线误判、双主 |
| Cluster | 集群节点 gossip | 分片内主从切换 | 分片迁移复杂,跨 slot 操作受限 |
脑裂成因:主库与哨兵失联但仍接受写入,同时另一侧选出新主,两边数据各自演进;旧主回归后被强制全量同步,那段写入就没了。缓解就是第 7 节那两个 min-replicas-*:宁可拒写,也不让旧主继续收写。
10. 备份与恢复:拷贝 RDB 的正确姿势
# 1) 等快照写完再拷,别 cp 正在写的 dump.rdb
redis-cli BGSAVE && redis-cli INFO persistence | grep rdb_bgsave_in_progress
# 2) 拷贝 + 校验 + 异地
cp /var/lib/redis/dump.rdb /backup/redis-$(date +%F-%H%M).rdb
redis-check-rdb /backup/redis-$(date +%F-%H%M).rdb
scp /backup/redis-*.rdb backup-host:/data/redis/
# 3) 恢复后核对
redis-cli DBSIZE && redis-cli GET some-known-key
| 错误做法 | 后果 |
|---|---|
直接 cp dump.rdb 不等 BGSAVE 完成 |
拷到半个文件,恢复时报 RDB 校验失败 |
| 只备份到本机 | 磁盘故障时备份与数据一起没 |
| 从不演练恢复 | 真出事时才发现文件不可用、dir 写错、权限不对 |
| 备份不分层 | 误删后,最近的备份里也已被删 |
11. 最小示例:开 AOF、看复制
redis-cli CONFIG SET appendonly yes # 7.x 会生成 appendonlydir/
redis-cli BGSAVE
redis-cli INFO persistence | grep -E 'rdb_last_bgsave_status|aof_last_write_status'
redis-server --port 6380 --replicaof 127.0.0.1 6379 # 一主一从
redis-cli -p 6380 INFO replication
观察四个字段:role、master_link_status(up 才算同步中)、两个 offset 的差值(延迟)、master_last_io_seconds_ago(心跳)。
# 制造延迟:往主库灌写,同时盯从库 offset
redis-cli -p 6379 --pipe < big-commands.txt
watch -n1 'redis-cli -p 6380 INFO replication | grep -E "offset|link_status"'
12. 排错:六个高频坑
| 现象 | 根因 | 处理 |
|---|---|---|
| 只开 RDB,丢最近数据 | 快照间隔内的写入无从恢复 | 开 AOF everysec 或缩短 save 间隔 |
| 重写期间磁盘打满 | 新旧文件并存,峰值近 2 倍 | 提前留空间,或错峰触发重写 |
| 全量同步压垮网络 | backlog 太小,断线后退化 | 加大 repl-backlog-size,开无盘复制 |
| 升级 7.x 后备份失效 | AOF 变成多文件目录 | 改拷 appendonlydir/ 整个目录 |
| 写命令全部报错 | 落盘失败 + stop-writes-on-bgsave-error |
先修磁盘与权限,别急着关开关 |
| 切换后数据"倒退" | 从库旧 + 无 min-replicas 保护 |
开 min-replicas-to-write,切换前比 offset |
13. 边界:Redis 不该当唯一存储
| 情况 | 说明 |
|---|---|
| 数据不可重建 | 持久化是"降低丢失"不是"保证不丢",权威数据要有主库(MySQL 等) |
| 需要事务与多 key 原子 | 持久化不解决这个,选型时就该换 |
| 强一致读 | 主从异步复制,天然有窗口,强一致读只能走主库 |
| 大内存实例(数十 GB) | fork 与 COW 代价显著,考虑分片或关 RDB 只留 AOF |
| 备份即合规要求 | 需要异地、加密、保留期策略,Redis 自带能力不够 |
14. 自测
BGSAVE的代价主要在哪一步?为什么内存越大越明显?appendfsync everysec最多丢多少数据?换成always要付什么代价?- AOF 重写的触发条件是什么?重写期间磁盘峰值为什么接近两倍?
- 从库断线多久之后会退化成全量同步?由哪个参数决定?
- 脑裂发生时,
min-replicas-to-write起什么作用?它换来的是什么代价? - 为什么"直接
cp dump.rdb"是错的?正确顺序是什么?