Files
cs-note/hhs/Redis/04-RDB持久化.md
T
2026-05-25 23:07:45 +08:00

602 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [Redis, 缓存, 持久化, RDB]
create time: 2026-05-15 18:12
---
# RDB 持久化
## 概述
> [!QUESTION] Redis 的数据都存在内存里,那服务器一断电,数据岂不是全没了?
> 没错——这就是**持久化**存在的意义。Redis 提供两种持久化方案:**RDB** 和 **AOF**,本文先讲 RDB。
> [!SUMMARY] 一句话理解 RDB
> RDB(Redis Database)就是在**某个时间点**,把 Redis 内存中的**全部数据**"拍一张照片"存到磁盘上——这个"照片"就是一个 `.rdb` **快照文件**。
>
> 你可以把它想象成**游戏存档**:打 Boss 之前你手动存了一次档,万一翻车了可以读档重来。但代价是——**存档之后到翻车之间**的操作全丢了。
### 核心特点
| 特性 | 说明 | 类比 |
|------|------|------|
| ⚡ 恢复极快 | 二进制文件直接加载到内存,跳过命令回放 | 像从压缩包解压,比照着菜谱重新做菜快得多 |
| 📦 文件紧凑 | 使用 LZF 压缩,远小于等量 AOF 日志 | 一张照片的信息量 > 千言万语的描述 |
| 🔧 架构简单 | 无额外复杂逻辑,一个文件搞定备份/迁移 | "复制-粘贴"级别的简单 |
| ⚠️ 数据窗口丢失 | 两次快照之间的写操作不可恢复 | 存档点之后的操作无法回溯 |
### 何时使用
- **大数据集冷备**:定期全量备份到 S3 / OSS,作为最后一道防线
- **灾难恢复**:服务器挂了?把 `.rdb` 文件丢到新机器上,秒级恢复
- **大规模数据场景**:当你的 Redis 存了 50GB 数据,AOF 回放可能要几十分钟,RDB 几秒搞定
- **主从复制**:新从节点首次全量同步时,主节点会自动触发 RDB 快照传给从节点
> [!QUESTION] 为什么 RDB 恢复比 AOF 快?
> 打个比方:RDB 就像**拍了一张照片**,加载时直接"看照片"还原场景;AOF 则像**记了一本流水账**("先加了张三,再删了李四,又改了王五……"),恢复时需要从头到尾把账本念一遍——数据量越大,"念账本"的时间就越长。
## 触发机制
### 手动触发
```bash
# 当前线程执行,阻塞主线程 ❌ 生产环境禁用!
SAVE
# 主进程 fork 子进程在后台完成(✅ 推荐)
BGSAVE
# 查看上次 BGSAVE 成功的时间戳
LASTSAVE
```
| 命令 | 行为 | 适用场景 | 风险 |
|------|------|----------|------|
| `SAVE` | 同步阻塞,直到快照完成 | 紧急调试,或确认实例无流量时 | **主线程完全卡死**——所有客户端请求排队等待 |
| `BGSAVE` | fork 子进程异步完成 | 生产环境日常备份 | fork 瞬间有极短暂的阻塞(通常毫秒级) |
> [!QUESTION] 为什么 SAVE 危险但 Redis 还要提供它?
> 因为 `SAVE` 可以在 **BGSAVE 正在执行时** 强制同步保存(BGSAVE 期间再次 BGSAVE 会被拒绝)。这是一个极端场景的"保底"手段。
### 自动触发——条件持久化
通过 `redis.conf` 配置多条 `save` 规则,Redis 用一个**"脏键计数器"(dirty counter)** 来追踪:
```conf
save 900 1 # 900 秒内至少有 1 个 key 被修改 → 触发快照
save 300 10 # 300 秒内至少有 10 个 key 被修改
save 60 10000 # 60 秒内至少有 10000 个 key 被修改
```
> [!INFO] 底层是怎么工作的?
> Redis 内部维护两个计数器:
> 1. **`dirty` 计数器**:自上次 BGSAVE 成功以来,总共被修改的 key 数量(每次写操作 +1)
> 2. **`lastsave` 时间戳**:上次成功完成 BGSAVE 的 Unix 时间戳
>
> 每隔 100ms(`serverCron` 定时任务),Redis 会检查:**当前时间 - lastsave ≥ 阈值时间 && dirty ≥ 阈值次数**,两个条件**同时满足**才会触发 BGSAVE。
>
> 所以 `save 900 1` 的意思是:"距离上次快照超过 900 秒**并且**期间有至少 1 个 key 被修改过"。如果 900 秒内没有任何写操作,即使规则匹配了也不会触发——因为没有新数据需要保存。
> [!QUESTION] 为什么要设三档而不是一档?
> 这是一个**渐进式频率**设计:
> - **低活跃度**(900s/1key):写入很少时,15 分钟存一次就够了——即使丢了 15 分钟的数据,也几乎没什么损失
> - **中活跃度**(300s/10key):中等写入压力时,5 分钟存一次,平衡了 I/O 开销和数据安全
> - **高活跃度**(60s/10000key):大量写入时,1 分钟存一次——虽然 fork 更频繁了,但此时丢 1 分钟的数据损失也更大
>
> 本质上是"**数据越重要、变更越频繁,保存就越勤**"的策略。
```bash
# 运行时动态修改(覆盖所有规则)
CONFIG SET save "900 1 300 10 60 10000"
# 临时关闭自动快照(谨慎!相当于禁用 RDB 自动触发)
CONFIG SET save ""
```
> [!WARNING] CONFIG SET 不持久化
> `CONFIG SET` 仅在内存中生效,Redis 重启后会恢复 `redis.conf` 的配置。若要永久生效,必须修改 `redis.conf` 并执行 `CONFIG REWRITE` 或手动重启。
### 其他自动触发场景
除了 `save` 规则外,以下场景也会自动触发 RDB:
| 场景 | 说明 | 为什么需要? |
|------|------|-------------|
| **正常关闭** | 执行 `SHUTDOWN` 时自动触发一次 BGSAVE,保证关闭前数据落盘 | 优雅关闭 ≠ 数据丢失 |
| **主从全量同步** | 新从节点首次连接主节点时,主节点自动触发 BGSAVE 并将快照传给从节点 | 从节点需要一份完整的数据副本 |
| **AOF 重写** | 开启混合持久化后,`BGREWRITEAOF` 会先写 RDB 快照作为基线 | 后面"混合持久化"章节详解 |
> [!TIP] Redis 如何避免"撞车"?
> Redis 同一时刻**只会有一个子进程在做持久化**。如果 BGSAVE 正在执行,此时再触发 SAVE/BGSAVE:
> - `BGSAVE` → 直接返回错误 `ERR Background save already in progress`
> - `SAVE` → 会等待 BGSAVE 完成后再执行
>
> 这个设计避免了多个子进程争抢 I/O 资源。
## RDB 文件结构
可以把 `.rdb` 文件想象成一个**快递包裹**:有快递单号(Header)、多个包裹(DB 里的数据)、包裹拆完的标记(EOF)、以及签收校验码(Footer)。
```mermaid
flowchart TD
subgraph header["1 HEADER 文件头"]
A["Magic: REDIS07 + Version"]
B["Auxiliary Fields: redis-ver, ctime, used-mem"]
C["DB Selector: FE 00(选择 DB 0)"]
A --> B --> C
end
subgraph data["2 DATA 数据段(每个非空 DB 重复一次)"]
D["Key Type: 1 byte(String=0, List=1, Set=2 ...)"]
E["Expire Info: 可选, TTL 时间戳"]
F["Encoded Key + Value: 直接序列化底层数据结构"]
D --> E --> F
F -.-> G["... 重复 N 个 Key-Value 对 ..."]
end
subgraph footer["3 FOOTER 文件尾"]
H["EOF Marker: 0xFF(数据结束)"]
I["CRC64 Checksum: 8 bytes(完整性校验)"]
H --> I
end
header --> data --> footer
```
### 各段含义详解
**1️⃣ Header(文件头)**
- **Magic 字符串**:以 `REDIS` 开头 + 4 字节版本号(如 `REDIS0009`),用于标识"这是一个 RDB 文件"
- **辅助字段(Auxiliary Fields)**:记录 Redis 版本、创建时间、内存占用等元信息——**不是数据本身,只是"快递单信息"**
- **DB Selector**:`FE` + DB 编号,告诉你"接下来的数据属于哪个 DB"(默认只有 DB 0 有数据,所以通常只出现一次)
**2️⃣ Data(数据段)**
每个 key-value 对的存储格式:
| 字段 | 大小 | 说明 |
|------|------|------|
| **Key Type** | 1 byte | 值的类型标记:String=0, List=1, Set=2, ZSet=3, Hash=4 … |
| **Expire Time** | 0 或 8 bytes | 如果 key 设置了过期时间,会附带一个毫秒级时间戳 |
| **Key** | 变长 | key 名(字符串编码) |
| **Value** | 变长 | **直接序列化底层数据结构**(ziplist、intset、skiplist 等),这是 RDB 文件紧凑的关键 |
**3️⃣ Footer(文件尾)**
- **EOF 标记**:`0xFF`,一个字节,宣告"数据到这里结束了"
- **CRC64 校验和**:8 字节,Redis 加载文件时用它校验完整性——如果校验失败,说明文件损坏,加载直接报错
> [!QUESTION] 为什么 RDB 文件比 AOF 小很多?
> 核心在于存储方式不同。举个例子:
>
> 假设你用 `HSET user:1 name "Alice" age 25 city "Beijing"` 存了一个 Hash:
> - **AOF 记录的是**:这条命令字符串本身 → `*8\r\n$5\r\nHSET\r\n$6\r\nuser:1\r\n$4\r\nname\r\n...\` 约 **120+ 字节**
> - **RDB 记录的是**:Hash 类型标记 + ziplist 编码的紧凑字节流 → 约 **30-40 字节**
>
> 差距来自:RDB 不记录"怎么做的",只记录"结果是什么"——就像一张照片远比描述照片的文字更紧凑。
## 关键配置参数
```conf
# ---- RDB 核心参数 ----
dbfilename dump.rdb # 快照文件名(建议保留默认)
dir /var/lib/redis # 工作目录——RDB/AOF 文件都存放在这里
rdbcompression yes # 是否使用 LZF 压缩字符串
rdbchecksum yes # 是否在文件末尾写入 CRC64 校验
rdb-save-incremental-fsync yes # 写入磁盘时是否增量 fsync
rdb-del-sync-files no # 无盘复制时,是否删除用于同步的临时 RDB(Redis 7+)
```
### 参数逐个解释
| 参数 | 推荐值 | 为什么? |
|------|--------|----------|
| `dbfilename` | `dump.rdb` | 保持默认即可。如果一台机器跑多个 Redis 实例,建议用端口号区分,如 `dump-6379.rdb` |
| `dir` | `/var/lib/redis` | 快照文件存放目录。**必须确保该目录有足够磁盘空间**,且 Redis 进程有写权限 |
| `rdbcompression` | `yes` | 开启 LZF 压缩可减少 40%-60% 的磁盘占用,代价是写入时多消耗一点 CPU。**绝大多数场景推荐开启** |
| `rdbchecksum` | `yes` | 开启后在文件末尾写入 CRC64 校验码,加载时会验证完整性。**关闭可略微提升加载速度,但不值得冒险** |
| `rdb-save-incremental-fsync` | `yes` | 开启后,子进程每写入 32MB 就执行一次 `fsync`,避免一次性刷盘造成 I/O 尖峰。对 SSD 尤其友好 |
> [!QUESTION] rdbcompression 关闭能提升多少性能?
> 压缩的 CPU 开销其实很小(LZF 是轻量压缩算法),瓶颈通常在磁盘 I/O 而非 CPU。关闭压缩后文件变大 → 写入时间更长 → 可能反而更慢。所以**除非你有明确的 CPU profiling 数据证明压缩是瓶颈,否则不要关**。
## Fork 过程详解
BGSAVE 的核心操作是 `fork()`——这是 Linux 的系统调用,能以**极低的成本**创建一份进程副本。整个流程如下:
```mermaid
sequenceDiagram
participant Client as 客户端
participant Main as 主进程
participant Child as 子进程
participant Disk as 磁盘
Client->>Main: 发送 BGSAVE 命令
Main->>Main: 创建子进程(fork)
Note over Main: fork 返回 0(子进程视角)<br/>fork 返回 pid(主进程视角)
Main->>Client: 返回 OK(立即响应)
par 主进程继续服务
Main->>Main: 正常处理读写请求
and 子进程执行快照
Child->>Child: 遍历所有 key-value
Child->>Disk: 序列化写入 dump.rdb.tmp
Note over Child,Disk: 写入过程中如果主进程修改了数据<br/>内核通过 COW 机制保护子进程视图
Child->>Main: 通知:快照完成
end
Main->>Disk: rename dump.rdb.tmp -> dump.rdb(原子替换)
Main->>Main: 更新 lastsave 时间戳, 重置 dirty 计数器
```
> [!TIP] 为什么 fork 这么快?
> `fork()` 并不会真的复制整个内存。Linux 使用**"写时复制"(Copy-On-Write, COW)**——fork 之后,父子进程**共享同一份物理内存页**,只在某一方**写入时**才真正拷贝那一页。所以 fork 一个 10GB 的 Redis 实例,实际瞬间开销只是创建页表(通常几十 MB),远小于 10GB。
### 深入理解 COW(Copy-On-Write)
> [!INFO] 图书馆类比
> 想象一个图书馆(物理内存),里面有很多书(内存页)。
> - **fork 之前**:只有主进程一个人在看书
> - **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] BGSAVE 期间 Redis 内存会翻倍吗?
> **不会翻倍,但最坏情况接近翻倍。**
>
> fork 本身**不复制任何内存页**。只有当主进程**实际写入**某一页时,那一页才会被 COW 拷贝。两种极端情况:
>
> | 场景 | 额外内存开销 |
> |------|-------------|
> | BGSAVE 期间主进程**几乎不写入** | ≈ 0(只共享,不拷贝) |
> | BGSAVE 期间主进程**全量写入** | ≈ 接近翻倍(每一页都被拷贝) |
> | 实际生产环境 | 通常 **10%-30%**(只有被修改的 key 对应的页才会 COW) |
> [!WARNING] fork 失败的常见原因
> fork 需要内核分配页表空间,如果系统内存不足会失败。关键运维参数:
> - **`vm.overcommit_memory = 1`**:告诉内核"允许过度提交",这是 Redis 官方推荐的 Linux 设置
> - **`vm.overcommit_ratio`**:默认 50,配合 overcommit_memory=2 使用
>
> 如果你看到 `Can't save in background: fork: Cannot allocate memory`,大概率是这个参数没设置对。
### TTL / 过期键处理
> [!QUESTION] RDB 快照会保存已过期的 key 吗?
> **最终不会。** 这是一个容易误解的点——很多人以为快照是"拍照片",会原封不动地保存内存里所有东西。实际上,Redis 通过**两道防线**确保过期键不会进入最终数据。
**第一道防线:子进程遍历时惰性跳过**
BGSAVE 触发时,Redis **不会**在 fork 前专门做一次"全量过期清理"(这在数据量大时太慢了)。实际流程是:
1. 主线程 fork 子进程
2. 子进程遍历所有 key,序列化写入 `.rdb` 文件
3. 在遍历每个 key 时,子进程会**检查 TTL 是否已过期**——如果已过期,**直接跳过,不写入文件**
> [!NOTE] 那日常的过期清理呢?
> Redis 平时通过两种机制清理过期键:
> - **惰性删除**:访问 key 时发现已过期 → 立即删除
> - **定期删除**:`serverCron` 每 100ms 随机抽样一批设置了 TTL 的 key,删除其中已过期的
>
> 所以到 BGSAVE 触发时,内存中**大部分**过期键其实已经被日常清理掉了。子进程遍历时再跳过漏网之鱼,双重保障。
**第二道防线:加载时二次检查**
即使某个 key 在快照时还没过期,但等到恢复加载时已经过期了——Redis 在加载 RDB 文件时会**再次检查 TTL**,过期的 key 不会被加载到内存。
```mermaid
flowchart LR
A["BGSAVE 触发"] --> B["Fork 子进程"]
B --> C["子进程遍历所有 Key"]
C --> D{"Key 是否<br/>已过期?"}
D -->|"是"| E["跳过,不写入 .rdb"]
D -->|"否"| F["序列化写入 .rdb"]
F --> G["RDB 文件只含有效数据"]
E --> C
style G fill:#c4e8b0
```
> [!QUESTION] 为什么要分两道防线,不能一次性清理干净?
> 因为**时间是流动的**。假设一个 key 的 TTL 还剩 1 秒时触发了 BGSAVE,子进程遍历时它还活着(写入了 .rdb),但 2 秒后加载时它已过期。所以加载时的二次检查是**必须的**——它处理的是"快照时还活着、但恢复时已过期"的时间差。
## 恢复流程
Redis 启动时会**自动检测** `dir` 目录下的 `.rdb` 文件并加载——你不需要手动执行任何"恢复命令"。
### 场景一:正常重启(最常见)
```bash
# 直接重启即可,Redis 自动加载 dump.rdb
redis-server /path/to/redis.conf
```
Redis 启动日志中你会看到类似:
```
* DB loaded from disk: 0.052 seconds # ← 52ms 加载完成
```
### 场景二:从备份恢复(灾难恢复)
当服务器故障需要从备份 `.rdb` 恢复时:
```bash
# 1. 停止当前 Redis(如果还在运行)
redis-cli SHUTDOWN NOSAVE # NOSAVE = 不再生成新快照,直接关闭
# 2. 把备份的 .rdb 文件放到 Redis 工作目录
cp /backup/dump.rdb /var/lib/redis/dump.rdb
# 3. 启动 Redis,它会自动加载这个 .rdb
redis-server /path/to/redis.conf
# 4. 验证数据恢复
redis-cli DBSIZE # 检查 key 总数
redis-cli INFO keyspace # 查看各 DB 的 key 数量和过期 key 数量
redis-cli GET some_known_key # 抽样验证已知 key 是否存在
```
### 场景三:迁移数据到新实例
```bash
# 在旧实例上手动触发一次快照
redis-cli BGSAVE
# 等待完成
redis-cli LASTSAVE # 观察时间戳变化
# 复制 .rdb 到新服务器
scp /var/lib/redis/dump.rdb user@new-server:/var/lib/redis/
# 在新服务器上启动 Redis(自动加载)
ssh user@new-server "redis-server /path/to/redis.conf"
```
> [!WARNING] 恢复时的常见坑
> 1. **文件权限**:确保 Redis 进程对 `dir` 目录有**读写权限**(启动时需要读取,运行时需要写入新快照)
> 2. **磁盘空间**:`.rdb` 文件恢复时需要临时解压到内存,确保服务器**可用内存 ≥ RDB 文件大小**
> 3. **同时存在 AOF 和 RDB**:如果 `dir` 下同时有 `dump.rdb` 和 `appendonly.aof`,**Redis 优先加载 AOF**(因为 AOF 数据通常更新)。如果你确实想用 RDB 恢复,需要先移走 AOF 文件
> 4. **版本兼容**:低版本 Redis 不能加载高版本生成的 RDB 文件(向后不兼容)
## RDB vs AOF 对比
| 维度 | RDB | AOF | 为什么? |
|------|-----|-----|----------|
| **数据安全性** | ⚠️ 可能丢失两次快照间的数据 | ✅ 最多丢 1 秒数据(everysec 策略) | RDB 是"定期拍照",AOF 是"实时写日记" |
| **恢复速度** | ⚡ 极快(直接加载二进制文件) | 🐢 较慢(逐条回放命令) | 读一本 100 页的书 vs 回放 100 万条操作日志 |
| **文件大小** | ✅ 小(压缩后的二进制) | ❌ 大(文本命令持续追加) | 照片 vs 逐帧视频——信息密度不同 |
| **CPU 开销** | 低(偶尔 fork) | 高(每条写命令都要追加) | 定期批量 vs 持续实时的天然差异 |
| **对写入性能影响** | 极小(fork 子进程异步完成) | 有影响(每次写都要 append) | BGSAVE 不阻塞主线程,AOF fsync 可能短暂阻塞 |
| **适用场景** | 备份、灾难恢复、主从同步 | 对数据安全要求高的在线服务 | 各取所长 |
> [!QUESTION] 生产环境怎么选?
> **答案是:不要二选一,两个都开。** Redis 4.0+ 引入的混合持久化(下文详解)完美解决了这个矛盾——RDB 保证恢复速度,AOF 保证数据安全。
## Redis 混合持久化(推荐)
### 为什么需要混合持久化?
回顾一下两者的痛点:
| 方案 | 优点 | 痛点 |
|------|------|------|
| RDB | 恢复快、文件小 | 两次快照间的数据可能丢失 |
| AOF | 数据几乎不丢 | 恢复慢、文件大 |
> [!QUESTION] 能不能把两者结合起来——用 RDB 加速恢复 + 用 AOF 保证数据安全?
> 可以!Redis 4.0 引入了**混合持久化(Hybrid Persistence)**,正是为了解决这个矛盾。
### 原理:AOF 文件 = RDB 快照 + 增量日志
开启混合持久化后,AOF **重写**时不再只写命令日志,而是:
```mermaid
flowchart LR
A["AOF 重写触发"] --> B["先写入一份 RDB 快照<br/>(覆盖重写时刻的全量数据)"]
B --> C["再追加重写期间的增量命令"]
C --> D["最终 AOF 文件"]
style D fill:#c4e8b0
```
也就是说,生成的 AOF 文件结构变成了:
```
┌──────────────────────────────────────┐
│ RDB 二进制快照(全量数据基线) │ ← 启动时直接加载,秒级完成
├──────────────────────────────────────┤
│ 增量 AOF 命令(快照之后的写操作) │ ← 逐条回放,保证数据不丢
└──────────────────────────────────────┘
```
> [!INFO] 用"写日记"来类比
> - **纯 RDB** = 定期拍照(恢复快,但照片之间的事不知道)
> - **纯 AOF** = 每天写日记(信息完整,但要从头读到尾才能恢复全部记忆)
> - **混合持久化** = 定期拍照 + 每天写日记。恢复时先看最近的照片(RDB 部分),再读照片之后的日记(AOF 部分)——既快又完整
### 如何开启
```conf
# redis.conf
aof-use-rdb-preamble yes # Redis 4.0+, 默认就是 yes
appendonly yes # 必须开启 AOF
```
### 恢复速度对比
```mermaid
flowchart LR
T1["传统 AOF 恢复<br/>回放数百万条命令"] -->|"O(N) 慢"| R1["耗时: 分钟级别"]
H1["RDB 快照加载<br/>秒级还原全量数据"] -->|"O(1) 快"| R2["耗时: 秒级"]
H2["回放少量增量 AOF<br/>仅重写后的命令"] -->|"极少"| R2
style R1 fill:#f9d0c4
style R2 fill:#c4e8b0
```
> [!TIP] 混合持久化几乎是"无脑开启"的配置
> - 恢复速度 ≈ RDB(因为大部分数据在 RDB 部分,AOF 增量部分很少)
> - 数据安全 ≈ AOF(增量日志保证不丢数据)
> - 开销 = AOF rewrite 的 fork 开销(和纯 AOF 一样)
>
> Redis 5.0+ 已默认开启,建议检查你的配置确认一下。
## 最佳实践
> [!SUMMARY] 核心原则:RDB 是备份手段,不是实时数据保障
> 不要把 RDB 当成"实时安全网"——它的定位是**定期快照 + 灾难恢复兜底**。实时数据安全交给 AOF。
1. **不要只依赖 RDB**:生产环境推荐 `AOF + 混合持久化`,AOF 保证数据安全,RDB 加速恢复
2. **合理设置 save 频率**:默认三档适合大部分场景。写入密集型可适当提高频率,但要注意:
- 频率越高 → fork 越频繁 → CPU 和内存压力越大
- 经验法则:`rdb_last_bgsave_time_sec` 超过 **1 秒**就需要关注
3. **备份策略(自动化)**:
```bash
#!/bin/bash
# redis-backup.sh — 每日定时备份脚本示例
REDIS_CLI="redis-cli"
BACKUP_DIR="/backup/redis"
DATE=$(date +%Y%m%d_%H%M%S)
# 1. 触发一次快照
$REDIS_CLI BGSAVE
# 2. 等待完成(轮询 lastsave)
LAST=$($REDIS_CLI LASTSAVE)
while [ "$($REDIS_CLI LASTSAVE)" == "$LAST" ]; do sleep 1; done
# 3. 拷贝并压缩
cp /var/lib/redis/dump.rdb "$BACKUP_DIR/dump-$DATE.rdb"
gzip "$BACKUP_DIR/dump-$DATE.rdb"
# 4. 保留最近 7 天的备份,删除更早的
find "$BACKUP_DIR" -name "dump-*.rdb.gz" -mtime +7 -delete
echo "Backup completed: dump-$DATE.rdb.gz"
```
4. **监控关键指标**:通过 `INFO persistence` 关注以下字段:
| 指标 | 含义 | 告警阈值建议 |
|------|------|-------------|
| `rdb_last_bgsave_status` | 上次 BGSAVE 是否成功 | ≠ `ok` 立即告警 |
| `rdb_last_bgsave_time_sec` | 上次 BGSAVE 耗时 | > 1s 关注,> 5s 告警 |
| `rdb_last_save_time` | 上次成功快照的时间戳 | 距今 > 1h 未触发则排查 |
| `rdb_changes_since_last_save` | 上次快照后的变更 key 数 | 持续增长说明 save 规则可能失效 |
5. **磁盘选择**:RDB 写入对 I/O 有要求,建议使用 SSD 并保持 `rdb-save-incremental-fsync yes`
> [!WARNING] 大 Key 是 RDB 的天敌
> 一个 100MB 的 String key,序列化时子进程写入耗时剧增(单个 key 可能就要写几秒),同时 COW 也会产生大量页面复制。
>
> **排查方法**:`redis-cli --bigkeys` 扫描大 key,发现后优先拆分。详见 [[hhs/Redis/11-运维与性能调优]]。
## 运维速查手册
```bash
# ---- 查看 RDB 状态 ----
INFO persistence
# 重点关注这些字段:
# rdb_last_bgsave_status: ok ← 上次是否成功
# rdb_last_bgsave_time_sec: 2 ← 上次耗时几秒
# rdb_last_save_time: 1715000000 ← 上次成功时间戳(Unix)
# rdb_changes_since_last_save: 1234 ← 自上次快照后的变更 key 数
# ---- 查看快照文件 ----
ls -lh /var/lib/redis/dump.rdb
# -rw-rw---- 1 redis redis 1.2G May 22 03:00 dump.rdb
# ---- 手动触发一次快照并等待完成 ----
redis-cli BGSAVE
# 等待:轮询 LASTSAVE 直到时间戳变化
while [ "$(redis-cli LASTSAVE)" == "$OLD" ]; do sleep 0.5; done
# ---- 检查 RDB 文件是否完整(不启动 Redis 的情况下) ----
# Redis 自带工具,用于检查 .rdb 文件的完整性
redis-check-rdb /var/lib/redis/dump.rdb
# 输出 OK 则文件完整,否则会告诉你哪里损坏了
```
## 常见故障排查
> [!WARNING] BGSAVE 失败?按这个顺序排查
> 以下是最常见的三类故障,按发生概率排序:
**1. fork 失败:`Can't save in background: fork: Cannot allocate memory`**
```bash
# 检查当前 overcommit 设置
cat /proc/sys/vm/overcommit_memory
# 如果输出 0 或 2 → 需要改为 1
# 临时修改(立即生效,重启失效)
echo 1 > /proc/sys/vm/overcommit_memory
# 永久修改(写入 sysctl)
echo "vm.overcommit_memory = 1" >> /etc/sysctl.conf && sysctl -p
```
> [!INFO] 为什么 overcommit_memory 必须设为 1?
> fork 需要内核分配页表空间,但此时并不会真正使用等量的物理内存(COW)。`overcommit_memory=0`(默认)时,内核会用启发式算法估算——对于大内存 Redis 实例,这个估算可能过于保守而拒绝 fork。设为 1 是告诉内核"不要猜了,直接允许"。
**2. 磁盘空间不足:写入 .rdb 时报错**
```bash
# 检查 dir 目录的可用空间
df -h /var/lib/redis
# 检查实际文件大小
ls -lh /var/lib/redis/dump.rdb
# 紧急处理:临时关闭 RDB 自动触发
redis-cli CONFIG SET save ""
```
**3. RDB 文件损坏:加载时 `Short read or OOM loading DB`**
```bash
# 使用 Redis 自带工具检查
redis-check-rdb /var/lib/redis/dump.rdb
# 如果文件损坏且有备份 → 用备份替换
# 如果没有备份 → 看 AOF 是否可用(Redis 优先加载 AOF)
```
## 安全性考虑
> [!WARNING] RDB 文件是明文存储的!
> `.rdb` 文件虽然看起来是"二进制",但其中的 key 名称和字符串类型的 value 都是**明文可读**的。用 `strings dump.rdb | head` 就能看到大部分内容。
**生产环境建议**:
1. **文件权限**:确保 `.rdb` 文件权限为 `600`(仅 Redis 用户可读写)
```bash
chmod 600 /var/lib/redis/dump.rdb
```
2. **备份加密**:备份到远端存储前,先加密再上传
```bash
# 使用 openssl 加密备份
openssl enc -aes-256-cbc -salt -pbkdf2 \
-in dump.rdb -out dump.rdb.enc -pass pass:$REDIS_BACKUP_KEY
```
3. **传输加密**:跨机器复制时使用 `scp` 或 `rsync over SSH`,避免明文传输
4. **敏感数据脱敏**:如果存了密码、Token 等敏感信息,建议在应用层加密后再写入 Redis——不要依赖 Redis 自身的"安全性"
> [!TIP] Redis 企业版(Redis Enterprise)支持透明的 RDB 文件加密
> 社区版不支持原生加密。如果你有强合规需求,可以考虑企业版或者在应用层做客户端加密。
## 关联笔记
- [[hhs/Redis/05-AOF持久化]] — AOF 持久化机制
- [[hhs/Redis/06-主从与哨兵]] — Sentinel 故障转移流程
- [[hhs/Redis/11-运维与性能调优]] — 备份策略与大 Key 治理