RDB 持久化原理

面试官问:"Redis 怎么实现持久化?"

小张说:"用 RDB 和 AOF。"

面试官追问:"RDB 是怎么生成的?"

小张说:"...fork 一个子进程?"

面试官继续追问:"fork 的过程中会不会阻塞?COW 是什么?"

小张开始支支吾吾。

RDB 是 Redis 持久化的核心机制之一。这道题能说清楚 fork、COW、BGSAVE 流程的候选人,对 Redis 的进程模型有较深的理解。

一、RDB 持久化概述 🔴

1.1 RDB 的原理

Redis 内存数据

fork 子进程(COPY ON WRITE)

子进程遍历内存,生成 RDB 文件

RDB 文件 = 内存数据的二进制快照

1.2 触发方式

-- 手动触发
SAVE       -- 阻塞主进程,生成 RDB 文件(不推荐)
BGSAVE     -- fork 子进程,后台异步生成 RDB 文件(推荐)

-- 自动触发(配置触发条件)
-- redis.conf:
save 900 1      -- 900 秒内 ≥1 个 key 变化
save 300 10     -- 300 秒内 ≥10 个 key 变化
save 60 10000   -- 60 秒内 ≥10000 个 key 变化

1.3 ❌ 错误示范

候选人原话:"BGSAVE 就是 Redis 在后台保存数据。"

问题诊断:BGSAVE 是 fork 一个子进程,由子进程生成 RDB 文件。主进程不阻塞,但 fork 过程本身可能有短暂阻塞。

候选人原话 2:"fork 复制了 Redis 的所有数据,内存会翻倍。"

问题诊断:混淆了 fork 和 COW。fork 只是复制了页表(不是完整数据),实际内存复制是 COW 机制控制的。

【面试官心理】 这道题我能从 fork 的过程追问。如果候选人能说清楚"fork 复制页表但共享物理内存,只有写入时才复制(COW)",说明他对 Linux 进程模型有深入理解。

二、fork 机制详解 🔴

2.1 fork 的过程

// Redis 调用 fork()
pid_t pid = fork();

// fork 返回值:
// - 主进程:返回子进程 pid(>0)
// - 子进程:返回 0

// 子进程继承了父进程的所有内存
// 但此时两个进程共享同一份物理内存

2.2 fork 的内存开销

-- fork 本身的开销:
-- - 复制父进程的页表(不是数据)
-- - 页表大小 = 进程地址空间 / 页大小 × 指针大小
-- - 4GB 地址空间 / 4KB 页 × 8 字节 = 8MB(可接受)

-- fork 不会复制数据,所以内存不会翻倍
-- 真正复制数据的是 COW(写时复制)

三、写时复制(Copy On Write)🟡

3.1 COW 原理

fork 之后:
┌─────────────────┐       ┌─────────────────┐
│     主进程       │       │     子进程       │
│   页表A          │       │   页表B          │
│   (共享物理内存)   │←←←←←←←│   (共享物理内存)   │
└─────────────────┘       └─────────────────┘
        ↓                         ↓
┌─────────────────────────────────────────┐
│           物理内存(共享)               │
│                                         │
│   Page1  Page2  Page3  Page4  Page5   │
└─────────────────────────────────────────┘

主进程写入 Page1 时:
┌─────────────────┐       ┌─────────────────┐
│   页表A          │       │   页表B          │
│   Page1 → 新页1  │       │   Page1 → 旧页1  │
│   (COW! 复制)    │       │   (指向旧页)     │
└─────────────────┘       └─────────────────┘

3.2 COW 的触发条件

-- COW 只在写入时触发
-- 读取不会触发 COW

-- Redis RDB 场景:
-- 1. fork 完成,父子进程共享所有物理页
-- 2. 子进程遍历内存,读取数据(不触发 COW)
-- 3. 主进程处理写请求,修改某个 key(触发 COW)
-- 4. 只有被修改的页才会复制

-- COW 的大小 ≈ 主进程修改的数据量
-- 如果主进程在 RDB 期间修改很少数据,COW 开销就很小

3.3 fork 阻塞的真正原因

-- fork 阻塞的两个原因:
-- 1. 复制父进程的页表(短暂,毫秒级)
-- 2. 如果 Redis 内存很大(>64GB),fork 可能需要较长时间

-- Linux 4.0+ 优化:
-- 开启 THP(Transparent Huge Pages)可以减少页表数量
-- 但 THP 可能导致内存碎片化

-- 建议:
-- 如果 Redis 内存 > 64GB,考虑:
-- 1. 关闭 THP
-- 2. 减少 Redis 内存使用
-- 3. 使用 AOF 代替 RDB

四、RDB 文件结构 🟡

4.1 RDB 文件格式

┌─────────────────────────────────────────────────────────┐
│ RDB 文件结构                                             │
│                                                          │
│ ┌──────┬──────────┬──────────┬────────────┬───────────┐ │
│ │魔数  │ 版本号    │ AUX字段  │ 数据库数据  │ EOF标志  │ │
│ │REDIS │ 0000~0010 │          │            │          │ │
│ │      │          │ RDB_OPTS │ SELECT DB  │          │ │
│ └──────┴──────────┴──────────┴────────────┴───────────┘ │
│                                                          │
│ AUX: 存储 Redis 版本、创建时间等元信息                    │
│ 数据库数据: SELECT DB + KEY-VALUE 对 + TYPE + ENCODING   │
│                                                          │
└─────────────────────────────────────────────────────────┘

4.2 压缩格式

-- RDB 文件可以被压缩
-- redis.conf:
rdbcompression yes  -- 启用 LZF 压缩(默认)

-- 压缩后的 RDB 文件通常只有原始大小的 20%~30%
-- 但压缩/解压需要 CPU 资源

五、生产避坑 🟢

5.1 RDB 的缺点

-- 缺点 1:可能丢失最后一次快照后的数据
-- 如果 Redis 崩溃,上一次 BGSAVE 之后的数据丢失
-- 解决:配合 AOF 使用

-- 缺点 2:fork 子进程开销大
-- Redis 内存 10GB,fork 需要复制页表
-- 如果内存很大,fork 可能需要 1~2 秒
-- 解决:使用 AOF 或降低 Redis 内存

-- 缺点 3:不适合实时数据
-- RDB 每分钟最多生成一次
-- 实时数据不适合只用 RDB

5.2 最佳配置

# redis.conf

# 自动触发条件(根据数据量设置)
save 900 1
save 300 10
save 60 10000

# 关闭自动触发,手动 BGSAVE
# (生产环境建议手动 + 定时任务)

# 压缩
rdbcompression yes

# 校验
rdbchecksum yes  # 生成 CRC64 校验和(轻微 CPU 开销)

【面试官心理】 RDB 持久化是 Redis 面试中的高频题。能说清楚 COW 机制和 fork 过程的候选人,说明他对 Linux 进程模型有实战理解。


级别考察重点期望回答
P5基本流程BGSAVE fork 子进程生成 RDB
P6机制理解fork 复制页表、COW 写时复制
P7深度权衡RDB vs AOF 配合、fork 阻塞原因、THP 优化