一次机房断电后重启,Redis 里少了 40 分钟的数据。查配置发现 save 是空的、appendonly 是 no——这台机器从上线起就没配持久化,只是它一直没重启过。
持久化这件事的残酷之处在于:它只在故障那天被考核,而考核结果无法补救。下面把 RDB、AOF、混合持久化、主从复制四条线拆开看,最后落到"我的业务该开哪个"。
1. RDB:fork 与写时复制
redis-cli CONFIG GET save # 例:3600 1 300 100 60 10000
redis-cli BGSAVE # 后台快照,返回 Background saving started
redis-cli INFO persistence | grep rdb_
BGSAVE 通过 fork() 复制一个子进程,子进程把当前内存页写成 RDB 文件;父进程继续处理命令,被修改的页由内核复制一份(COW),所以快照期间写入越多,内存占用越高。
| 项 | 说明 |
|---|---|
save(前台) |
阻塞所有命令,只有测试或关库时才用 |
bgsave(后台) |
主流方式,fork 的瞬间仍有短暂阻塞 |
| COW 内存放大 | 写入比例越高放大越明显,最坏可达原内存的两倍量级 |
fork 失败 |
物理内存或 overcommit 不足时失败,INFO 里 rdb_last_bgsave_status:err |
| 恢复速度 | 加载 RDB 是顺序解析,通常比回放 AOF 快 |
两个必须做的系统配置:vm.overcommit_memory=1(否则 fork 可能因"内存不足"直接失败)、关闭透明大页(THP 会放大 fork 的页表复制开销)。这也是为什么内存使用率长期超过 60% 的实例不适合跑大快照。
2. AOF:三种 appendfsync 策略
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec # always | everysec | no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-use-rdb-preamble yes # 混合持久化开关
| 策略 | 数据丢失窗口 | 性能影响 | 适用 |
|---|---|---|---|
always |
理论上 0(每条命令落盘) | 每次写都要 fsync,吞吐显著下降 |
金额流水、不可丢的计数 |
everysec |
最多约 1 秒(默认) | 每秒一次 fsync,影响可控 |
绝大多数业务 |
no |
交给 OS,通常数十秒量级 | 几乎无影响 | 可重建的数据 |
AOF 重写(BGREWRITEAOF)不是"重写日志",而是根据当前内存状态生成一份最小命令集,所以重写后的文件通常远小于原文件。Redis 7 使用多部分 AOF(manifest + base + incr 文件),重写期间新命令写入新的 incr 文件,避免旧版本重写缓冲区过大。
3. 混合持久化:RDB 打底 + AOF 增量
开启 aof-use-rdb-preamble(Redis 5 起默认)后,AOF 文件开头是一段 RDB 格式的全量快照,后面追加增量命令。效果是:
| 维度 | 纯 AOF | 混合 |
|---|---|---|
| 恢复速度 | 慢(逐条回放) | 快(先加载 RDB 再回放尾部) |
| 文件体积 | 大 | 小 |
| 可读性 | 文本可读 | 前半段二进制,人读不了 |
代价是"文件开头不再是明文",用文本工具排查 AOF 的习惯要改;以及重写时仍然要 fork,内存放大的问题并没有消失。
4. 两种持久化对照
| 维度 | RDB | AOF |
|---|---|---|
| 数据丢失窗口 | 一个 save 周期(可能十几分钟) |
取决于 appendfsync,最差 1 秒 |
| 恢复速度 | 快 | 慢(混合后接近 RDB) |
| 文件体积 | 小(二进制压缩) | 大(可用重写压缩) |
| 对主线程影响 | fork 瞬间阻塞 + COW 内存 |
fsync 抖动 + 重写时的 fork |
| 适合 | 纯缓存、可重建数据、备份归档 | 有状态业务、不可丢的写 |
工程上常见的组合是 appendonly yes + aof-use-rdb-preamble yes + 保留 save 做定期冷备:AOF 保近期不丢,RDB 文件拿去对象存储做归档。
5. 主从复制的流程
# 从库上执行
REPLICAOF 10.0.0.11 6379
INFO replication # 看 master_link_status、master_repl_offset、slave_repl_offset
复制分两个阶段:
| 阶段 | 做什么 | 关键参数 |
|---|---|---|
| 全量同步 | 主库生成 RDB(可走磁盘或无盘 repl-diskless-sync),传输后从库加载,期间命令进缓冲区 |
repl-diskless-sync、repl-diskless-sync-delay |
| 增量同步 | PSYNC <replid> <offset> 续传,主库从 backlog 里补发 |
repl-backlog-size(默认 1mb,生产常调大) |
repl-backlog-size 是断线重连能否续传的分水岭:网络抖动 30 秒、写入量又大,backlog 覆盖不住这段位移,从库就只能全量重同步,代价是一次 fork + 一次全量传输。写流量大的实例,backlog 按"峰值写入速率 × 期望容忍的断开时长"估。
6. 复制延迟与 WAIT
redis-cli -h 10.0.0.11 WAIT 1 100 # 等 1 个副本确认,最多 100ms
redis-cli -h 10.0.0.11 INFO replication | grep -E 'lag|offset'
异步复制下"写主库、立刻读从库"会读到旧值。WAIT 能让当前连接阻塞到指定数量的副本确认,但它有明确边界:
| 边界 | 说明 |
|---|---|
| 只保证"此刻已确认",不保证故障转移后不丢 | 主库挂掉时,未复制部分仍然消失 |
| 需要客户端主动调用 | 不是全局开关,漏调的写就没有保护 |
| 超时会返回实际确认数 | 返回值小于期望值时业务要自己决定重试还是继续 |
真正需要"写完立刻能读到"的地方,做法是读主库(或同一会话粘在主库),而不是给所有写加 WAIT——那等于把复制的收益又还回去。
7. 哨兵与集群的故障转移
| 维度 | 哨兵(Sentinel) | 集群(Cluster) |
|---|---|---|
| 数据分片 | 无,单主承载全量 | 16384 个 slot 分片 |
| 故障检测 | 多个哨兵独立判定 + quorum | 集群总线 gossip |
| 选主 | 哨兵从副本中挑一个提升 | 该分片的副本提升 |
| 客户端 | 连哨兵问主地址 | 按 slot 重定向(MOVED/ASK) |
| 最小规模 | 3 哨兵 + 1 主 1 从 | 3 主 3 从 |
| 主要代价 | 全量数据受单机内存限制 | 多 key 操作受 slot 限制 |
故障转移一定伴随数据丢失窗口,根因是异步复制:主库已向客户端返回 OK、但写命令还没到副本时宕机,这部分数据在新主上不存在。分片越多、写入越密,窗口里的数据越多。
8. 内存上限与淘汰策略
maxmemory 8gb
maxmemory-policy allkeys-lru # 纯缓存
# maxmemory-policy noeviction # 当数据库用,写满就报错
| 策略 | 行为 | 适用 |
|---|---|---|
noeviction |
达到上限后写命令直接报错 | 有状态数据,宁可报错也不丢 |
allkeys-lru / lfu |
在所有 key 里按最近/最常使用淘汰 | 纯缓存 |
volatile-lru / ttl |
只在设了 TTL 的 key 里淘汰 | 混合负载,缓存与状态共存 |
allkeys-random |
随机淘汰 | 访问分布均匀的场景 |
选 allkeys-* 前必须确认这个实例里没有"不能被淘汰"的数据。一个常见事故是把会话、分布式锁和缓存放在同一实例,淘汰策略一开,锁被淘汰,互斥直接失效。另外可以用 min-replicas-to-write + min-replicas-max-lag 阻止"只剩自己一个"的主库继续接受写入,减少脑裂期的数据分叉。
9. 什么场景只开 RDB
| 场景 | 建议 |
|---|---|
| 纯缓存,数据可从 DB 重建 | 只开 RDB 甚至不开,重启就是冷缓存(需配预热) |
| 会话、购物车等"丢了要重新登录" | AOF everysec |
| 计数、排行、去重(不可重建) | AOF everysec + 定期 RDB 归档 |
| 金额、流水 | AOF always,且必须有 DB 侧对账 |
| 允许分钟级丢失的统计指标 | 只开 RDB,save 周期按可接受丢失量定 |
判断标准只有一句:这份数据丢了,能不能从别处重新算出来。能,就 RDB;不能,就 AOF;两者都不能承受,就不该只放在 Redis 里。
10. 常见坑
| 坑 | 后果 | 正确做法 |
|---|---|---|
| 内存 90% 还开着大快照 | fork 时 COW 分配失败,主库直接不可写 |
留 40% 以上余量,或改用无盘复制 |
appendfsync always |
fsync 抖动打满磁盘 IOPS,写入延迟飙升 |
只在必要数据上用,且单独实例 |
| 从库被写 | 主从数据分叉,切换后数据错乱 | replica-read-only yes,禁用直接写从库 |
| 故障转移后配置漂移 | 哨兵重写 sentinel.conf、集群重写 nodes.conf,人工改的被覆盖 |
变更走 CONFIG SET + CONFIG REWRITE,纳入版本管理 |
| AOF 与 RDB 同时重写 | IO 争抢,主线程 fsync 变慢 |
错开时间窗,或调低重写触发频率 |
| 只做备份不做恢复演练 | 真故障时发现 RDB 文件损坏/版本不兼容 | 定期在隔离环境实际加载一次 |
11. 排错
| 现象 | 原因 | 处理 |
|---|---|---|
rdb_last_bgsave_status:err |
磁盘满 / fork 失败 / 目录权限 |
看日志 Can't save in background,清磁盘或调 overcommit |
| 重启后数据比预期少很多 | AOF 尾部截断(aof-load-truncated) |
检查 redis-check-aof --fix,确认丢的是哪一段 |
从库一直 master_link_status:down |
网络不通 / requirepass 不一致 / 版本过高无法 PSYNC |
核对密码与 replid,必要时 REPLICAOF 重连 |
| 复制延迟持续上涨 | 主库写入过猛、从库单线程回放跟不上 | 加 repl-backlog-size,或拆分写入热点 |
| 内存突然翻倍 | 快照期间 COW 放大 | 避开高峰 BGSAVE,降低写入峰值 |
| 故障转移后部分 key 消失 | 异步复制窗口 | 属预期,业务侧需幂等 + 对账 |
12. 边界
| 需求 | 更合适的方案 |
|---|---|
| 要求 RPO = 0 | 单机 Redis 做不到,需同步复制或换共识存储 |
| 数据量超过单机内存 | 集群分片,或改用磁盘型存储 |
| 需要事务性多 key 写 | 单实例/同 slot,跨 slot 事务不存在 |
| 长期归档与审计 | 落数仓/对象存储,Redis 不是归档系统 |
持久化解决的是"重启后数据还在",复制解决的是"机器挂了服务还在",两者都不解决"数据一定不丢"。把这三件事分开想,选型就不会纠结。
13. 自测
BGSAVE期间的写时复制为什么会放大内存?放大到什么程度最危险?appendfsync三种策略各自的丢失窗口大致是多少?为什么always也可能丢?repl-backlog-size太小会引发什么后果?怎么估这个值?WAIT能保证故障转移后数据不丢吗?它保证的是什么?- 什么情况下应该选
noeviction,而不是allkeys-lru? - 一个存分布式锁的实例被配成
allkeys-lru,会发生什么?