2903 字
约 9 分钟
13
S12-X09 · 持久化与主从:RDB、AOF 与数据安全的取舍

一次机房断电后重启,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. 自测

  1. BGSAVE 期间的写时复制为什么会放大内存?放大到什么程度最危险?
  2. appendfsync 三种策略各自的丢失窗口大致是多少?为什么 always 也可能丢?
  3. repl-backlog-size 太小会引发什么后果?怎么估这个值?
  4. WAIT 能保证故障转移后数据不丢吗?它保证的是什么?
  5. 什么情况下应该选 noeviction,而不是 allkeys-lru?
  6. 一个存分布式锁的实例被配成 allkeys-lru,会发生什么?
S12-X09 · 持久化与主从:RDB、AOF 与数据安全的取舍
https://www.clxhxhhr.top/posts/5820/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。