290 lines
11 KiB
Markdown
290 lines
11 KiB
Markdown
---
|
||
tags: [Redis, 缓存, 持久化, RDB]
|
||
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 则需要逐条回放命令——数据量越大差距越明显。
|
||
|
||
## 触发机制
|
||
|
||
### 手动触发
|
||
|
||
```bash
|
||
# 当前线程执行,阻塞主线程 ❌
|
||
SAVE
|
||
|
||
# 主进程 fork 子进程在后台完成(推荐)
|
||
BGSAVE
|
||
|
||
# 查看上次 BGSAVE 状态
|
||
LASTSAVE
|
||
```
|
||
|
||
| 命令 | 行为 | 影响 |
|
||
|------|------|------|
|
||
| `SAVE` | 同步保存,主线程阻塞 | **生产环境禁用!** |
|
||
| `BGSAVE` | fork 子进程异步保存 | 主线程继续服务,但触发 fork 瞬间短暂停顿 |
|
||
| `BGREWRITEAOF` | AOF rewrite | 与 BGSAVE 可同时运行(不同子进程) |
|
||
|
||
### 自动触发——条件持久化
|
||
|
||
通过 `redis.conf` 配置:
|
||
|
||
```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 高频大量变更时快速落盘
|
||
|
||
```bash
|
||
# 运行时动态修改
|
||
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+ 引入了新格式,更紧凑且支持增量复制:
|
||
|
||
```mermaid
|
||
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 命令可能有数百字节。
|
||
|
||
## 关键配置参数
|
||
|
||
```conf
|
||
# ---- 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 过程详解
|
||
|
||
```mermaid
|
||
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 后父子进程共享同一份物理内存页(只读),只有当某一方**写入**时,内核才会真正复制该页——这就是"写时复制"。
|
||
|
||
```mermaid
|
||
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。
|
||
|
||
具体流程:
|
||
1. `BGSAVE` 触发时,主线程先将当前所有已过期 key 从内存中删除
|
||
2. 子进程 fork 完成后遍历剩余 key → 序列化到 .rdb 文件
|
||
3. 因此:**RDB 文件中不会出现已过期键**
|
||
|
||
```mermaid
|
||
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 重写时会主动过滤过期键,恢复时也只会加载有效数据。两者优先保证启动时的数据一致性。
|
||
|
||
## 恢复流程
|
||
|
||
```bash
|
||
# 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 的优点:
|
||
|
||
```conf
|
||
aof-use-rdb-preamble yes
|
||
```
|
||
|
||
开启后,AOF rewrite 不再只追加命令日志,而是先写入一份 RDB 快照,再追加后续命令:
|
||
|
||
```mermaid
|
||
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 是备份手段,不是实时数据保障
|
||
|
||
1. **不要只依赖 RDB**:生产环境推荐 `AOF + RDB` 混合持久化,AOF 保证数据安全性,RDB 保证快速恢复
|
||
2. **合理设置触发频率**:默认三档(900s/300s/60s)适合大部分场景;写入密集型业务可适当提高频率,但注意 fork 开销
|
||
3. **监控 fork 耗时**:`INFO persistence` 中的 `rdb_last_bgsave_time_sec`,超过 1s 就需要关注
|
||
4. **备份策略**:定期将 `.rdb` 文件拷贝到异地存储(S3/OSS),建议保留多个历史版本
|
||
5. **磁盘选择**:RDB 文件写入对磁盘 I/O 有要求,建议使用 SSD 并配合 `rdb-save-incremental-fsync`
|
||
|
||
> [!WARNING] 大 Key 是 RDB 的天敌
|
||
> 一个 100MB 的 String key,序列化时会导致子进程写入耗时剧增,同时 COW 也可能产生大量页面复制。发现大 Key 后应优先拆分。
|
||
|
||
## 运维要点
|
||
|
||
```bash
|
||
# 监控 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 治理
|