11 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
2026-05-15 18:12 |
RDB 持久化
概述
[!SUMMARY] RDB 是什么? RDB(Redis Database)是 Redis 最早的持久化机制,通过在特定时间点将内存中的数据集写入磁盘形成快照文件(.rdb),实现数据持久化。
核心特点
| 特性 | 说明 |
|---|---|
| ⚡ 恢复极快 | 二进制文件直接加载,避免命令回放 |
| 📦 文件紧凑 | 压缩存储,远小于等量 AOF 日志 |
| 🔧 架构简单 | 无额外复杂逻辑,适合备份和迁移 |
| ⚠️ 数据窗口丢失 | 两次快照之间的写操作不可恢复 |
何时使用
- 大数据集冷备:定期全量备份到 S3 / OSS
- 灾难恢复:配合 CI/CD 快速重建实例
- 大规模数据场景:恢复速度优先于毫秒级数据一致性
[!QUESTION] 为什么 RDB 恢复比 AOF 快? RDB 是直接加载二进制快照到内存,AOF 则需要逐条回放命令——数据量越大差距越明显。
触发机制
手动触发
# 当前线程执行,阻塞主线程 ❌
SAVE
# 主进程 fork 子进程在后台完成(推荐)
BGSAVE
# 查看上次 BGSAVE 状态
LASTSAVE
| 命令 | 行为 | 影响 |
|---|---|---|
SAVE |
同步保存,主线程阻塞 | 生产环境禁用! |
BGSAVE |
fork 子进程异步保存 | 主线程继续服务,但触发 fork 瞬间短暂停顿 |
BGREWRITEAOF |
AOF rewrite | 与 BGSAVE 可同时运行(不同子进程) |
自动触发——条件持久化
通过 redis.conf 配置:
save 900 1 # 900 秒内至少有 1 个 key 被修改 → 触发快照
save 300 10 # 300 秒内至少有 10 个 key 被修改
save 60 10000 # 60 秒内至少有 10000 个 key 被修改
[!QUESTION] 为什么要三档条件?
- 低活跃度:900s/1key 保证最终一致
- 中活跃度:300s/10key 平衡频率和数据量
- 高活跃度:60s/10000key 高频大量变更时快速落盘
# 运行时动态修改
CONFIG SET save "900 1\n300 10\n60 10000"
# 临时关闭自动快照(谨慎!)
CONFIG SET save ""
[!WARNING] CONFIG SET 不持久
CONFIG SET仅在内存中生效,重启后失效。修改redis.conf并 reload 才是持久化的方式。
其他触发场景
| 场景 | 说明 |
|---|---|
| shutdown 保存 | SHUTDOWN / SHUTDOWN NOSAVE 会在关闭前执行一次完整 RDB 保存(除非显式传 NOSAVE) |
| AOF rewrite | BGREWRITEAOF 重写 AOF 时同样会 fork 子进程生成 RDB preamble(开启混合持久化时) |
| 主从同步 | 新从节点首次全量同步时,主节点会自动触发 BGSAVE 并将快照传给从节点 |
[!TIP] 避免频繁触发 多个触发条件同时满足时,Redis 会去重,仅执行一次 BGSAVE。若已有 BGSAVE 在执行,后续的 SAVE 请求将被忽略。
RDB 文件结构
Redis 7.0+ 引入了新格式,更紧凑且支持增量复制:
flowchart TD
H1["Header"] --> H2["Magic: REDIS"]
H1 --> H3["RDB Version"]
H1 --> H4["Reserved / CRC64"]
D1["DB Number<br/>1 byte"] --> D2["Key Type<br/>1 byte"]
D2 --> D3["Encoded Key + Value"]
E1["EOF Marker"] --> E2["CRC64 Checksum<br/>8 bytes"]
H1 & H2 & H3 & H4 ==> "Dataset Section"
D1 & D2 & D3 ==> "Dataset Section"
D3 -.-> "... N keys ..."
E1 & E2 ==> "Footer"
各段含义
| 段落 | 内容 | 说明 |
|---|---|---|
| Header | Magic + Version + 校验 | REDIS 魔数标识文件类型;版本号用于兼容性判断;CRC64 用于完整性校验 |
| DB Selector | 数据库编号 | 默认 16 个 DB(0-15),只序列化非空 DB |
| Key-Value Pairs | 类型 + 编码 + 数据 | 每个 key 携带类型标记(String/List/Hash/ZSet…),底层编码直接序列化,因此文件极紧凑 |
| EOF | 0xFF 终止符 |
标识数据段结束 |
| Footer | CRC64 校验和 | 8 字节,启动加载时校验文件完整性 |
[!QUESTION] 为什么 RDB 文件比 AOF 小很多? 因为 RDB 直接序列化底层数据结构(如 ziplist、intset),而非记录产生这些数据的命令字符串。一个 Hash 用 ziplist 存储可能只有几十字节,但生成它的 HSET 命令可能有数百字节。
关键配置参数
# ---- RDB 核心参数 ----
dbfilename dump.rdb # 快照文件名
dir /var/lib/redis # 工作目录,RDB/AOF 文件存放位置
rdbcompression yes # 是否使用 LZF 压缩字符串(推荐开启,CPU vs 磁盘权衡)
rdbchecksum yes # 是否在文件末尾写入 CRC64 校验(推荐开启)
rdb-save-incremental-fsync yes # 写入磁盘时增量 fsync,避免一次性大量 I/O 阻塞
rdb-del-sync-files no # 无盘复制时,是否删除用于同步的临时 RDB(Redis 7+)
[!TIP] rdbcompression 的取舍 开启压缩可以显著减少磁盘占用(通常压缩 40%-60%),代价是写入时多消耗一点 CPU。对于绝大多数场景,保持 yes 是最优解。只有在 CPU 极度敏感、磁盘充裕的特殊场景下才考虑关闭。
Fork 过程详解
sequenceDiagram
participant Server as Redis Server
participant Parent as 主进程
child ForkGroup as Forked Child
participant Child as 写时复制子进程
participant Disk as 磁盘 (.rdb)
end
Server->>Parent: BGSAVE 指令
Parent->>Server: OK(立即返回)
Parent->>Child: fork() 创建子进程
Note over Child: 子进程独享父进程<br/>内存副本(写时复制 COW)
Child->>Disk: 遍历所有 key → 序列化
Note over Child: 主进程仍可读写<br/>COW 机制按需拷贝脏页
Child->>Parent: 生成临时 .rdb.tmp
Parent->>Disk: RENAME 原子替换
Parent->>Server: BGSAVE 完成通知
[!WARNING] COW 内存消耗 fork 后主进程若持续写入大值,会导致 COW 副本膨胀。例如你有一个 1GB 的值被频繁修改,fork 后每次写都可能多拷贝一页(4KB)。高峰期建议关闭自动 RDB,仅依赖 AOF。
深入理解 COW(Copy-On-Write)
fork 后父子进程共享同一份物理内存页(只读),只有当某一方写入时,内核才会真正复制该页——这就是"写时复制"。
flowchart TD
F["Fork 完成"] --> S["父子进程共享<br/>所有物理内存页"]
S --> W{"主进程收到<br/>写请求?"}
W -->|"否"| R["子进程继续读取<br/>(零拷贝, 极快)"]
W -->|"是"| C["内核复制该内存页<br/>(Copy-On-Write)"]
C --> N["主进程写入副本<br/>子进程仍读原页"]
R & N --> D["子进程完成序列化"]
D --> DONE["RDB 文件生成"]
[!QUESTION] 为什么 Redis 占用内存会翻倍? 这是面试高频题。答案是:不会翻倍,但最坏情况接近翻倍。 fork 本身不复制内存,只有主进程实际写入的页面才会被 COW。如果 BGSAVE 期间主进程几乎不写入,额外内存开销几乎为零。但若全量写入,就会逐步逼近 2 倍。
运维经验:保持系统
vm.overcommit_memory=1(Linux 默认),否则 fork 可能因内核认为内存不足而失败。
TTL / 过期键处理
[!QUESTION] RDB 快照会保存已过期的 key 吗? 不会。 Redis 在 fork 子进程前会先执行一次主动过期扫描,清理掉所有已超时的 key。
具体流程:
BGSAVE触发时,主线程先将当前所有已过期 key 从内存中删除- 子进程 fork 完成后遍历剩余 key → 序列化到 .rdb 文件
- 因此:RDB 文件中不会出现已过期键
flowchart LR
A["BGSAVE 触发"] --> B["主线程: 主动过期<br/>清理超时 Key"]
B --> C["Fork 子进程"]
C --> D["子进程: 遍历剩余数据<br/>序列化到 .rdb"]
D --> E[".rdb 无过期键"]
style E fill:"#c4e8b0"
[!NOTE] 过期策略差异 虽然 RDB 不保存过期键,但 AOF 可能包含过期命令(如之前的 SET k v EX 100)。AOF 重写时会主动过滤过期键,恢复时也只会加载有效数据。两者优先保证启动时的数据一致性。
恢复流程
# 1. 停止 Redis
redis-cli SHUTDOWN NOSAVE
# 2. 把 .rdb 放到 redis_dir(由 dir 配置决定)
cp dump.rdb /var/lib/redis/
# 3. 启动 Redis
redis-server --daemonize yes
# 4. 验证数据恢复
redis-cli DBSIZE
redis-cli KEYS "*"
RDB vs AOF 对比
| 维度 | RDB | AOF |
|---|---|---|
| 数据安全性 | 两次快照间数据丢失 | 每秒/每次可配置,接近零丢失 |
| 恢复速度 | ⚡ 极快(直接加载二进制文件) | 🐢 较慢(需要回放日志) |
| 文件大小 | ✅ 小(紧凑压缩) | ❌ 大(文本命令日志) |
| CPU 开销 | 低(偶尔 fork) | 高(持续追加写入) |
| 适用场景 | 备份、灾难恢复、大数据集冷备 | 对可用性要求高的在线服务 |
| 重启耗时 | 秒级 | 取决于日志长度(可 pre-fork) |
Redis 7 混合持久化
结合 RDB + AOF 的优点:
aof-use-rdb-preamble yes
开启后,AOF rewrite 不再只追加命令日志,而是先写入一份 RDB 快照,再追加后续命令:
flowchart LR
T1["传统 AOF 重建<br/>逐条命令回放"] -->|"O(N) 慢"| R1["重建耗时: 长"]
H1["RDB 二进制快照<br/>全量快速还原"] -->|"秒级加载"| R2["重建耗时: 短"]
H2["AOF 增量日志<br/>仅保留变更后操作"] -->|"精确到毫秒"| R2
style R1 fill:"#f9d0c4"
style R2 fill:"#c4e8b0"
[!TIP] 混合持久化几乎是无脑开启的配置——启动快 + 不丢数据。
最佳实践
[!SUMMARY] 核心原则:RDB 是备份手段,不是实时数据保障
- 不要只依赖 RDB:生产环境推荐
AOF + RDB混合持久化,AOF 保证数据安全性,RDB 保证快速恢复 - 合理设置触发频率:默认三档(900s/300s/60s)适合大部分场景;写入密集型业务可适当提高频率,但注意 fork 开销
- 监控 fork 耗时:
INFO persistence中的rdb_last_bgsave_time_sec,超过 1s 就需要关注 - 备份策略:定期将
.rdb文件拷贝到异地存储(S3/OSS),建议保留多个历史版本 - 磁盘选择:RDB 文件写入对磁盘 I/O 有要求,建议使用 SSD 并配合
rdb-save-incremental-fsync
[!WARNING] 大 Key 是 RDB 的天敌 一个 100MB 的 String key,序列化时会导致子进程写入耗时剧增,同时 COW 也可能产生大量页面复制。发现大 Key 后应优先拆分。
运维要点
# 监控 RDB 相关指标
INFO persistence
# rdb_last_bgsave_status: OK
# rdb_last_save_time: 1715000000 ← 最后成功时间戳
# 检查文件大小
ls -lh /var/lib/redis/dump.rdb
# 压缩备份(已内置 LZ4 压缩,通常不需要额外压缩)
tar czf redis-backup.tar.gz dump.rdb
关联笔记
- hhs/Redis/05-AOF持久化 — AOF 持久化机制
- hhs/Redis/06-主从与哨兵 — Sentinel 故障转移流程
- hhs/Redis/11-运维与性能调优 — 备份策略与大 Key 治理