RDB快照持久化
Redis RDB快照持久化:save与bgsave机制、写时复制原理、配置优化与恢复流程
1. RDB 概述
RDB(Redis Database)是 Redis 的默认持久化方式,通过在指定时间间隔内对数据集进行快照(snapshot),将内存中的数据以二进制文件形式写入磁盘。生成的 RDB 文件紧凑、体积小,非常适合用于备份和灾难恢复。
RDB 的核心优势:
- 紧凑高效:RDB 文件是经过压缩的二进制格式,体积远小于 AOF 日志
- 恢复速度快:直接加载二进制文件,恢复大数据集的速度远快于 AOF
- 对性能影响小:
bgsave由子进程完成,主进程几乎不受影响 - 适合冷备:可以定期将 RDB 文件归档到远程存储
RDB 的主要不足:
- 数据丢失风险:非实时持久化,两次快照之间的数据可能丢失
- fork 开销:数据集很大时,
fork()子进程可能耗时较长 - 不支持增量:每次都是全量快照,无法记录增量变更
2. save 命令
save 命令以阻塞方式生成 RDB 文件:
SAVE
执行流程:
- 主进程直接调用
rdbSave()函数 - 主进程被阻塞,无法响应任何客户端请求
- RDB 文件写入完成后,主进程恢复服务
适用场景:
- 数据集很小,阻塞时间可忽略
- 需要确保快照完成的测试环境
- Redis 正在关闭时(shutdown 触发 save)
风险:生产环境中数据集较大时,save 可能导致数秒甚至数十秒的阻塞,严重影响可用性。
3. bgsave 命令
bgsave(Background Save)是生产环境推荐的方式,通过 fork() 子进程完成快照:
BGSAVE
执行流程:
- 主进程调用
fork()创建子进程 - 子进程调用
rdbSave()将数据写入临时 RDB 文件 - 写入完成后,子进程将临时文件原子性地重命名为正式 RDB 文件
- 子进程退出,主进程收到信号
主进程 ──fork()──> 子进程
│ │
│ 继续处理请求 │ rdbSave()
│ │ 写入临时文件
│ │ rename()
│<──SIGCHLD──────── │ 退出
│
└── 更新 rdb_save_time_last
查看 bgsave 状态:
LASTSAVE # 返回最近一次成功保存的 Unix 时间戳
INFO Persistence # 查看 rdb 相关统计信息
bgsave 期间的保护机制:
- bgsave 执行期间,再次调用
bgsave会被拒绝(ERR Background save already in progress) save命令同样会被拒绝,避免产生竞争条件
4. 写时复制(Copy-On-Write)
写时复制(COW)是 bgsave 能够在不阻塞主进程的情况下生成一致性快照的关键机制。
4.1 fork 与内存共享
fork() 创建子进程后,父子进程共享同一块物理内存页。此时内存消耗并不会翻倍,因为页表指向相同的物理帧。
4.2 COW 触发条件
当主进程尝试修改某个共享页时:
- 内核检测到该页被标记为只读(共享页)
- 触发缺页异常(Page Fault)
- 内核复制该页的副本给主进程
- 主进程在副本上执行修改
- 子进程仍然读取原始页的数据
fork() 后:
父进程页表 → 物理页 A (只读)
子进程页表 → 物理页 A (只读)
父进程修改页 A:
父进程页表 → 物理页 A' (可写) ← 新副本
子进程页表 → 物理页 A (只读) ← 原始数据
4.3 内存开销估算
COW 的额外内存开销取决于 bgsave 期间被修改的页面数量:
Linux 默认页大小为 4KB。假设 bgsave 耗时 5 秒,每秒写入 10 万个 key,每个 key 平均 100 字节:
生产建议:预留 Redis 实例内存的 20%~50% 作为 COW 缓冲。
5. 自动触发配置
Redis 通过 save 配置项实现自动触发 RDB 快照:
# 在 redis.conf 中配置
save 900 1 # 900秒内有至少1个key被修改
save 300 10 # 300秒内有至少10个key被修改
save 60 10000 # 60秒内有至少10000个key被修改
触发逻辑:满足任意一个条件即触发 bgsave。
# 禁用自动 RDB
save ""
# 运行时修改
CONFIG SET save "900 1 300 10 60 10000"
5.1 自动触发条件评估
Redis 每秒执行一次 serverCron(),检查是否满足 save 条件:
- 遍历所有
save配置项 - 计算自上次成功保存以来的时间间隔和修改 key 数量
- 如果满足任一条件,触发
bgsave
6. RDB 文件格式
RDB 文件采用二进制格式,结构如下:
┌──────────┬──────────┬───────────┬──────────┬──────────┐
│ REDIS │ version │ databases │ EOF │ checksum │
│ 魔数 │ 版本号 │ 数据库数据 │ 结束标记 │ 校验和 │
└──────────┴──────────┴───────────┴──────────┴──────────┘
各数据库区域结构:
┌─────────────┬──────────────────┬──────────────────┐
│ SELECTDB │ key-value对 │ key-value对 │
│ 数据库编号 │ (带过期时间) │ (无过期时间) │
└─────────────┴──────────────────┴──────────────────┘
编码优化:
- 小整数使用变长编码(1/2/4 字节)
- 短字符串使用嵌入编码(len + data)
- LZF 压缩长字符串
- 整数集合(intset)直接存储
- 压缩列表(ziplist/listpack)直接存储
7. RDB 文件恢复
7.1 自动恢复
Redis 启动时自动检测 RDB 文件:
- 读取
dir和dbfilename配置 - 打开 RDB 文件,校验 checksum
- 逐个加载 key-value 到内存
- 加载完成后开始接受客户端请求
# 配置 RDB 文件路径
dir /var/lib/redis
dbfilename dump.rdb
7.2 RDB 文件校验
# 使用 redis-check-rdb 工具
redis-check-rdb dump.rdb
7.3 恢复注意事项
- RDB 文件加载期间 Redis 处于阻塞状态
- 如果 RDB 文件损坏,Redis 可能拒绝启动
- 可以使用
redis-check-rdb --fix尝试修复
8. 配置优化
8.1 关键配置项
# RDB 文件名
dbfilename dump.rdb
# 存储目录
dir /var/lib/redis
# 是否压缩(LZF)
rdbcompression yes
# 是否使用 CRC64 校验
rdbchecksum yes
# bgsave 失败时是否停止写入
stop-writes-on-bgsave-error yes
# 自动触发条件
save 900 1
save 300 10
save 60 10000
8.2 性能调优建议
| 场景 | save 配置 | 说明 |
|---|---|---|
| 高可用(允许少量丢失) | save 60 10000 | 减少快照频率,降低 I/O 压力 |
| 数据安全(尽量少丢失) | save 60 1 | 增加快照频率,增加 I/O 开销 |
| 纯缓存(可丢失) | save "" | 禁用 RDB,最大化性能 |
| 大数据集 | 适当放宽间隔 | 减少 fork 开销 |
8.3 大内存实例优化
当 Redis 实例使用内存超过 10GB 时:
fork()耗时可能超过 1 秒- COW 可能占用数 GB 额外内存
- 建议拆分为多个小实例,或使用 AOF 替代
- 考虑开启
lazyfree-lazy-eviction减少主线程阻塞
9. RDB 与 AOF 对比
| 特性 | RDB | AOF |
|---|---|---|
| 持久化方式 | 定时快照 | 实时追加日志 |
| 数据安全性 | 可能丢失数分钟数据 | 最多丢失 1 秒数据 |
| 文件体积 | 小(压缩二进制) | 大(文本日志) |
| 恢复速度 | 快 | 慢 |
| 性能影响 | fork 开销 | 写入开销 |
| 适用场景 | 备份、灾难恢复 | 数据安全优先 |