欢迎来到窥视未来的博客

https://github.com/lwx57280 https://gitee.com/li_VillageHead

线上 Redis 因为 SAVE 命令阻塞了 30 秒,我翻遍源码才弄懂 SAVE 和 BGSAVE 的本质区别

兄弟们,你们遇到过这种场景吗?Redis 突然卡死几十秒,所有请求超时,监控显示 CPU 和内存都没问题,但就是没有任何响应。我那次就是因为执行了一个 SAVE 命令——当时只是想手动备份一下数据,没想到直接把整个服务干瘫痪了。

后来我翻遍了 Redis 源码,才彻底弄明白 SAVE 和 BGSAVE 这两个命令的本质区别。今天就把这次“血的教训”和源码级的分析完整复盘出来。

一、事故还原:一个 SAVE 命令引发的惨案

那天下午,运维同学想在 Redis 上手动做一个快照备份,执行了 SAVE 命令。Redis 实例内存 8GB,RDB 文件生成耗时约 30 秒。这 30 秒内,Redis 完全无法处理任何客户端请求。

监控显示:QPS 直接归零,所有业务接口超时,上游服务报警不断。

mermaid-1782632404921

 

 

事后我们复盘:这个实例是生产环境的缓存服务,任何阻塞操作都是不可接受的。正确的做法应该是用 BGSAVE,而不是 SAVE。

二、SAVE vs BGSAVE:一张表看懂核心区别

 
对比维度SAVEBGSAVE
阻塞性 同步阻塞,主进程无法处理任何请求 异步非阻塞,主进程继续处理请求
执行方式 主进程直接调用 rdbSave fork 子进程,子进程调用 rdbSave
返回时机 RDB 文件生成完成后才返回 OK 立即返回 Background saving started
生产环境 ❌ 禁止使用 ✅ 推荐使用
适用场景 紧急备份、关机前保存 定期持久化、生产环境备份

mermaid-1782631512136

 

三、源码级分析:两者到底怎么执行的?

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() 时,操作系统会创建一个子进程。这个子进程会复制父进程的页表(记录虚拟地址与物理地址的映射关系),而不是复制实际的内存数据。

mermaid-1782631991649

 父子进程共享同一块物理内存,直到某个进程尝试写入数据时,操作系统才会触发写时复制(Copy-on-Write, COW) 。

4.2 写时复制如何保证数据一致性?

在 BGSAVE 执行期间:

  1. 子进程:读取内存数据,生成 RDB 快照,只读不写

  2. 父进程:继续处理客户端请求,可能写入新数据

mermaid-1782632068478

 核心原理:子进程在 fork 的那一刻“冻结”了数据快照。父进程后续的修改不会影响子进程正在读取的内存页,因为 COW 机制会为被修改的页创建副本。

4.3 BGSAVE 期间的内存开销

虽然 fork 时只复制了页表(很小),但随着父进程持续写入,COW 会不断复制被修改的内存页。如果写入量大,内存占用可能翻倍。

生产建议:

  • 在内存使用率较低时执行 BGSAVE(建议 < 50%)

  • 避免在高峰期执行 BGSAVE

  • 监控内存使用,防止 OOM

五、命令组合规则:哪些命令不能同时执行?

Redis 在执行 BGSAVE 期间,对某些命令有特殊限制:

场景行为原因
BGSAVE 执行中,收到 SAVE ❌ 被拒绝 防止父子进程同时执行 rdbSave,产生竞争条件
BGSAVE 执行中,收到 BGSAVE ❌ 被拒绝 防止多个子进程同时执行 RDB 生成
BGSAVE 执行中,收到 BGREWRITEAOF ⚠️ 延迟到 BGSAVE 完成后执行 避免同时进行磁盘 I/O 操作

mermaid-1782632184238

 

六、生产环境最佳实践

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 压力

  • 强校验机制:提升数据完整性

七、避坑总结

  1. 生产环境永远不要用 SAVE——它会阻塞所有请求,几分钟的阻塞就可能造成灾难性后果

  2. BGSAVE 是唯一选择——fork 子进程异步执行,主进程不受影响

  3. BGSAVE 也有代价——fork 复制页表 + COW 复制内存页,内存使用率高峰期要谨慎执行

  4. Redis 7.0 性能更强——多线程 RDB 能大幅缩短大实例的备份时间

  5. 监控 BGSAVE 执行状态——通过 LASTSAVE 和 INFO persistence 确认备份是否成功

兄弟们,你们在生产环境中有没有因为 SAVE 命令踩过坑?或者遇到过 BGSAVE 期间内存翻倍的问题?评论区聊聊,我帮你们分析分析。

posted on 2026-06-28 15:40  k8s-Mango  阅读(20)  评论(0)    收藏  举报

导航