线上 Redis 因为 SAVE 命令阻塞了 30 秒,我翻遍源码才弄懂 SAVE 和 BGSAVE 的本质区别
兄弟们,你们遇到过这种场景吗?Redis 突然卡死几十秒,所有请求超时,监控显示 CPU 和内存都没问题,但就是没有任何响应。我那次就是因为执行了一个 SAVE 命令——当时只是想手动备份一下数据,没想到直接把整个服务干瘫痪了。
后来我翻遍了 Redis 源码,才彻底弄明白 SAVE 和 BGSAVE 这两个命令的本质区别。今天就把这次“血的教训”和源码级的分析完整复盘出来。
一、事故还原:一个 SAVE 命令引发的惨案
那天下午,运维同学想在 Redis 上手动做一个快照备份,执行了 SAVE 命令。Redis 实例内存 8GB,RDB 文件生成耗时约 30 秒。这 30 秒内,Redis 完全无法处理任何客户端请求。
监控显示:QPS 直接归零,所有业务接口超时,上游服务报警不断。

事后我们复盘:这个实例是生产环境的缓存服务,任何阻塞操作都是不可接受的。正确的做法应该是用 BGSAVE,而不是 SAVE。
二、SAVE vs BGSAVE:一张表看懂核心区别
| 对比维度 | SAVE | BGSAVE |
|---|---|---|
| 阻塞性 | 同步阻塞,主进程无法处理任何请求 | 异步非阻塞,主进程继续处理请求 |
| 执行方式 | 主进程直接调用 rdbSave |
fork 子进程,子进程调用 rdbSave |
| 返回时机 | RDB 文件生成完成后才返回 OK | 立即返回 Background saving started |
| 生产环境 | ❌ 禁止使用 | ✅ 推荐使用 |
| 适用场景 | 紧急备份、关机前保存 | 定期持久化、生产环境备份 |

三、源码级分析:两者到底怎么执行的?
3.1 共同的底层函数:rdbSave
无论是 SAVE 还是 BGSAVE,最终都会调用 rdbSave 函数来执行实际的快照生成工作(RDB设计)。
// Redis 源码 rdb.c 中的核心函数
int rdbSave(char *filename, rdbSaveInfo *rsi) {
// 创建临时文件
// 遍历所有数据库,序列化所有 key-value
// 写入 RDB 文件
// 重命名为正式文件
return retval;
}
两者的核心区别在于谁来调用这个函数——是主进程自己调用,还是 fork 出来的子进程调用。
3.2 SAVE:主进程直接调用,同步阻塞
SAVE 命令的实现非常直接:
// SAVE 命令的处理函数(简化)
void saveCommand(client *c) {
// 直接在主进程上下文中调用 rdbSave
if (rdbSave(server.rdb_filename, NULL) == C_OK) {
addReply(c, shared.ok);
} else {
addReplyError(c, "ERR save failed");
}
}
关键点:rdbSave 是一个同步函数,执行期间会占用主进程,Redis 无法处理任何其他命令。
3.3 BGSAVE:fork 子进程,异步执行
BGSAVE 的实现要复杂得多:
// BGSAVE 命令的处理函数(简化)
void bgsaveCommand(client *c) {
// 检查是否有 BGSAVE 正在执行
if (server.rdb_child_pid != -1) {
addReplyError(c, "Background save already in progress");
return;
}
// fork 子进程
pid_t child_pid = fork();
if (child_pid == 0) {
// 子进程:执行 rdbSave
// 关闭与客户端的连接,避免干扰
// 调用 rdbSave 生成 RDB 文件
// 完成后退出
exitFromChild(0);
} else {
// 父进程:记录子进程 PID,继续处理请求
server.rdb_child_pid = child_pid;
// 更新统计信息
server.stat_rdb_saves++;
addReply(c, shared.ok);
}
}
关键点:主进程 fork 后立即返回,子进程在后台执行耗时的 RDB 生成操作。
3.3 BGSAVE:fork 子进程,异步执行
BGSAVE 的实现要复杂得多:
// BGSAVE 命令的处理函数(简化)
void bgsaveCommand(client *c) {
// 检查是否有 BGSAVE 正在执行
if (server.rdb_child_pid != -1) {
addReplyError(c, "Background save already in progress");
return;
}
// fork 子进程
pid_t child_pid = fork();
if (child_pid == 0) {
// 子进程:执行 rdbSave
// 关闭与客户端的连接,避免干扰
// 调用 rdbSave 生成 RDB 文件
// 完成后退出
exitFromChild(0);
} else {
// 父进程:记录子进程 PID,继续处理请求
server.rdb_child_pid = child_pid;
// 更新统计信息
server.stat_rdb_saves++;
addReply(c, shared.ok);
}
}
关键点:主进程 fork 后立即返回,子进程在后台执行耗时的 RDB 生成操作。
四、深入理解 fork 与写时复制(Copy-on-Write)
4.1 fork 做了什么?
当 Redis 主进程执行 fork() 时,操作系统会创建一个子进程。这个子进程会复制父进程的页表(记录虚拟地址与物理地址的映射关系),而不是复制实际的内存数据。

父子进程共享同一块物理内存,直到某个进程尝试写入数据时,操作系统才会触发写时复制(Copy-on-Write, COW) 。
4.2 写时复制如何保证数据一致性?
在 BGSAVE 执行期间:
-
子进程:读取内存数据,生成 RDB 快照,只读不写
-
父进程:继续处理客户端请求,可能写入新数据

核心原理:子进程在 fork 的那一刻“冻结”了数据快照。父进程后续的修改不会影响子进程正在读取的内存页,因为 COW 机制会为被修改的页创建副本。
4.3 BGSAVE 期间的内存开销
虽然 fork 时只复制了页表(很小),但随着父进程持续写入,COW 会不断复制被修改的内存页。如果写入量大,内存占用可能翻倍。
生产建议:
-
在内存使用率较低时执行 BGSAVE(建议 < 50%)
-
避免在高峰期执行 BGSAVE
-
监控内存使用,防止 OOM
五、命令组合规则:哪些命令不能同时执行?
Redis 在执行 BGSAVE 期间,对某些命令有特殊限制:
| 场景 | 行为 | 原因 |
|---|---|---|
| BGSAVE 执行中,收到 SAVE | ❌ 被拒绝 | 防止父子进程同时执行 rdbSave,产生竞争条件 |
| BGSAVE 执行中,收到 BGSAVE | ❌ 被拒绝 | 防止多个子进程同时执行 RDB 生成 |
| BGSAVE 执行中,收到 BGREWRITEAOF | ⚠️ 延迟到 BGSAVE 完成后执行 | 避免同时进行磁盘 I/O 操作 |

六、生产环境最佳实践
6.1 命令选择
| 场景 | 推荐命令 | 原因 |
|---|---|---|
| 生产环境手动备份 | BGSAVE | 不阻塞服务 |
| 服务器关机前保存 | SAVE | 此时服务本就要停,无所谓阻塞 |
| 定时自动备份 | BGSAVE(通过配置文件) | 非阻塞 |
| 主从复制全量同步 | BGSAVE(自动) | Master 自动执行 |
线上禁止使用 SAVE:这是血的教训。
6.2 配置建议
# redis.conf 配置自动 BGSAVE save 900 1 # 900秒内至少1次修改 → BGSAVE save 300 10 # 300秒内至少10次修改 → BGSAVE save 60 10000 # 60秒内至少10000次修改 → BGSAVE[reference:25] # Redis 7.0 多线程 RDB 优化(内存 >16GB 时建议开启) rdbthreads 4 # 使用 4 个线程并行生成 RDB[reference:26]
6.3 监控与巡检
# 查看 RDB 生成状态 LASTSAVE # 返回最后一次成功生成 RDB 的时间戳 # 查看是否有 BGSAVE 正在执行 INFO persistence # 关注 rdb_bgsave_in_progress 字段
6.4 Redis 7.0 的优化
Redis 7.0 对 RDB 持久化做了重要优化:
-
多线程序列化:使用多个线程并行生成 RDB,加快大实例的备份速度
-
增量刷盘:减少单次 I/O 压力
-
强校验机制:提升数据完整性
七、避坑总结
-
生产环境永远不要用 SAVE——它会阻塞所有请求,几分钟的阻塞就可能造成灾难性后果
-
BGSAVE 是唯一选择——fork 子进程异步执行,主进程不受影响
-
BGSAVE 也有代价——fork 复制页表 + COW 复制内存页,内存使用率高峰期要谨慎执行
-
Redis 7.0 性能更强——多线程 RDB 能大幅缩短大实例的备份时间
-
监控 BGSAVE 执行状态——通过
LASTSAVE和INFO persistence确认备份是否成功
兄弟们,你们在生产环境中有没有因为 SAVE 命令踩过坑?或者遇到过 BGSAVE 期间内存翻倍的问题?评论区聊聊,我帮你们分析分析。
浙公网安备 33010602011771号