602 lines
28 KiB
Markdown
602 lines
28 KiB
Markdown
---
|
||
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 治理
|