--- 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(子进程视角)
fork 返回 pid(主进程视角) Main->>Client: 返回 OK(立即响应) par 主进程继续服务 Main->>Main: 正常处理读写请求 and 子进程执行快照 Child->>Child: 遍历所有 key-value Child->>Disk: 序列化写入 dump.rdb.tmp Note over Child,Disk: 写入过程中如果主进程修改了数据
内核通过 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["父子进程共享
所有物理内存页"] S --> W{"主进程收到
写请求?"} W -->|"否"| R["子进程继续读取原页
(零拷贝,极快)"] W -->|"是"| C["内核复制该内存页
(Copy-On-Write)"] C --> N["主进程写入副本页
子进程仍读原页"] 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 是否
已过期?"} 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 快照
(覆盖重写时刻的全量数据)"] 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 恢复
回放数百万条命令"] -->|"O(N) 慢"| R1["耗时: 分钟级别"] H1["RDB 快照加载
秒级还原全量数据"] -->|"O(1) 快"| R2["耗时: 秒级"] H2["回放少量增量 AOF
仅重写后的命令"] -->|"极少"| 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 治理