Redis save 和 bgsave 命令

Redis 源码版本:redis-6.2.17

前提

命令数组

Redis 支持的命令定义在全局数组 redisCommandTable 中。

struct redisCommand redisCommandTable[] = {
    .....

    {"save",saveCommand,1,
   "admin no-script",
   0,NULL,0,0,0,0,0,0},

    {"bgsave",bgsaveCommand,-1,
   "admin no-script",
   0,NULL,0,0,0,0,0,0},

    {"bgrewriteaof",bgrewriteaofCommand,1,
   "admin no-script",
   0,NULL,0,0,0,0,0,0},

    {"shutdown",shutdownCommand,-1,
   "admin no-script ok-loading ok-stale",
   0,NULL,0,0,0,0,0,0},

    {"lastsave",lastsaveCommand,1,
   "random fast ok-loading ok-stale @admin @dangerous",
   0,NULL,0,0,0,0,0,0},
    
    ....
}

子进程类型

宏常量定义了 Redis 中的子进程类型。宏常量定义在 server.h

宏常量对应子进程类型应用场景
CHILD_TYPE_NONE0无活跃子进程默认状态,表示当前未执行任何需要子进程的任务。
CHILD_TYPE_RDB1RDB持久化子进程执行 SAVEBGSAVE 命令时创建,用于生成 RDB 快照文件。
CHILD_TYPE_AOF2AOF重写子进程执行 BGREWRITEAOF 命令时创建,用于压缩/重写 AOF 文件以优化体积和恢复速度。
CHILD_TYPE_LDB3Lua调试器子进程在 Redis 调试模式(如 redis-cli --ldb)下启动,用于隔离调试环境与主进程。
CHILD_TYPE_MODULE4模块自定义子进程由 Redis 模块(Module)创建,用于扩展功能(如异步任务处理)。

Redis 同一时间最多(尽最大努力保证)只运行一种类型的子进程(子进程类型由 server.child_type 标识)。

save

save 语法:

SAVE

SAVE 命令对数据集进行同步保存,以 RDB 文件的形式生成 Redis 实例中所有数据的时间点快照。

在生产环境中,不要想着调用 SAVE ,因为它会阻塞所有其他客户端。通常使用 BGSAVE 。然而,如果出现问题阻止 Redis 创建后台 SAVE 子进程(例如 fork(2) 系统调用出错),SAVE 命令可以是执行最新数据集转储的最后手段。

源码

save 命令调用 saveCommand 函数进行处理,该函数位于 rdb.c

save 命令会导致 RDB 操作在主进程中执行。

void saveCommand(client *c) {
    // 已经存在后台子进程在执行RDB
    if (server.child_type == CHILD_TYPE_RDB) {
        addReplyError(c,"Background save already in progress");
        return;
    }
    rdbSaveInfo rsi, *rsiptr;
    rsiptr = rdbPopulateSaveInfo(&rsi);
    // rdbSave()实际生成RDB文件
    if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {
        addReply(c,shared.ok);
    } else {
        addReplyErrorObject(c,shared.err);
    }
}

调用 rdbSave() 生成 RDB 文件。


/* Save the DB on disk. Return C_ERR on error, C_OK on success. */
int rdbSave(char *filename, rdbSaveInfo *rsi) {
    char tmpfile[256];
    char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */
    FILE *fp = NULL;
    rio rdb;
    int error = 0;

    // 临时RDB文件名,避免直接覆盖现有RDB文件
    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    fp = fopen(tmpfile,"w");	// 打开该临时文件
    if (!fp) {
        char *cwdp = getcwd(cwd,MAXPATHLEN); // 获取当前工作目录
        serverLog(LL_WARNING,
            "Failed opening the RDB file %s (in server root dir %s) "
            "for saving: %s",
            filename,
            cwdp ? cwdp : "unknown",
            strerror(errno));
        return C_ERR;
    }

    // 将文件流包装为 Redis I/O 对象(rio),支持缓冲写入
    rioInitWithFile(&rdb,fp);
    // 向Redis持久化模块发布RDB开始事件
    startSaving(RDBFLAGS_NONE);

    // 如果启用rdb_save_incremental_fsync,按固定字节数自动同步数据到磁盘,避免内存堆积
    if (server.rdb_save_incremental_fsync)
        rioSetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES);

    // 遍历内存数据库,将键值对按RDB二进制格式序列化到文件
    //   1. 写入数据库头部元数据(如版本号、复制信息)
    //   2. 按数据类型(String、Hash、List 等)逐个序列化数据
    if (rdbSaveRio(&rdb,&error,RDBFLAGS_NONE,rsi) == C_ERR) {
        errno = error;
        goto werr;
    }

    /* Make sure data will not remain on the OS's output buffers */
    if (fflush(fp)) goto werr;	// 强制将C库缓冲区数据写入操作系统缓冲区
    if (fsync(fileno(fp))) goto werr;	//强制数据从操作系统缓冲区刷入物理磁盘
    if (fclose(fp)) { fp = NULL; goto werr; } // 关闭文件
    fp = NULL;
    
    /* Use RENAME to make sure the DB file is changed atomically only
     * if the generate DB file is ok. 
     * 通过rename系统调用将临时RDB文件重命名为目标RDB文件名(如dump.rdb)*/
    if (rename(tmpfile,filename) == -1) {
        char *cwdp = getcwd(cwd,MAXPATHLEN);
        serverLog(LL_WARNING,
            "Error moving temp DB file %s on the final "
            "destination %s (in server root dir %s): %s",
            tmpfile,
            filename,
            cwdp ? cwdp : "unknown",
            strerror(errno));
        unlink(tmpfile); // 删除临时RDB文件
        stopSaving(0);	// 向Redis持久化模块发布生成RDB失败事件
        return C_ERR;
    }

    serverLog(LL_NOTICE,"DB saved on disk");
    // server.dirty记录自上次持久化后的数据修改次数,归零表示没有需要持久化的变更。
    server.dirty = 0;
    //lastsave更新为当前时间,用于监控工具(如INFO persistence)报告最后一次成功持久化时间
    server.lastsave = time(NULL);
    // lastbgsave_status记录最新一次save的状态
    server.lastbgsave_status = C_OK;
    // 向Redis持久化模块发布生成RDB成功事件
    stopSaving(1);
    return C_OK;

werr:
    serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno));
    if (fp) fclose(fp);
    unlink(tmpfile);
    stopSaving(0);
    return C_ERR;
}

bgsave

bgsave 语法:

BGSAVE [SCHEDULE]

后台(异步)保存数据库。

通常会立即返回 OK 代码。Redis 进行 fork(2) 操作,父进程继续为客户端提供服务,子进程将数据库保存到磁盘然后退出。

如果已经有后台 SAVE 操作正在进行,或者有其他非后台保存进程正在运行,特别是有正在进行的 AOF 重写操作时,将返回错误。

如果使用了 BGSAVE SCHEDULE 命令,当有 AOF 重写操作正在进行时,该命令将立即返回 OK,并计划在下次有机会时运行后台 SAVE 操作。

客户端可以使用 LASTSAVE 命令检查该操作是否成功。

源码

bgsaveCommand

save 命令调用 bgsaveCommand 函数进行处理,该函数位于 rdb.c

/* BGSAVE [SCHEDULE] */
void bgsaveCommand(client *c) {
    int schedule = 0;

    /* The SCHEDULE option changes the behavior of BGSAVE when an AOF rewrite
     * is in progress. Instead of returning an error a BGSAVE gets scheduled. */
    if (c->argc > 1) {
        if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) {
            schedule = 1;
        } else {
            addReplyErrorObject(c,shared.syntaxerr);
            return;
        }
    }

    rdbSaveInfo rsi, *rsiptr;
    rsiptr = rdbPopulateSaveInfo(&rsi);

    // 已经有后台的RDB子进程在运行
    if (server.child_type == CHILD_TYPE_RDB) {
        addReplyError(c,"Background save already in progress");
    } else if (hasActiveChildProcess()) {	// 其它的子进程在运行
        // 执行形如 BGSAVE SCHEDULE 命令
        if (schedule) {
            server.rdb_bgsave_scheduled = 1;	// 设置标志,用于延后调用
            addReplyStatus(c,"Background saving scheduled");
        } else {
            addReplyError(c,
            "Another child process is active (AOF?): can't BGSAVE right now. "
            "Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever "
            "possible.");
        }
    } else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) {
        addReplyStatus(c,"Background saving started");
    } else {
        addReplyErrorObject(c,shared.err);
    }
}

先判断是否已经有后台的 RDB 子进程在运行了,如果是,返回响应信息 "Background save already in progress"

再调用 hasActiveChildProcess() 检查是否有其它子进程在运行,若已有活跃子进程(如正在执行 BGSAVEBGREWRITEAOF 等),如果调用 BGSAVE 时设置了 SCHEDULE 选项,则设置调度标志,等其它子进程运行完后在执行 RDB。

/* Return true if there are active children processes doing RDB saving,
 * AOF rewriting, or some side process spawned by a loaded module. */
int hasActiveChildProcess() {
    return server.child_pid != -1;
}

如果没有其他子进程在运行后,调用 rdbSaveBackground 进一步处理。

rdbSaveBackground

int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    pid_t childpid;
    // 再次判断是否有其它子进程在运行
    if (hasActiveChildProcess()) return C_ERR;
    // 记录持久化前的数据修改次数,用于后续统计(如INFO persistence的rdb_changes_since_last_save)
    server.dirty_before_bgsave = server.dirty;
    // 记录本次BGSAVE尝试时间,用于监控延迟。
    server.lastbgsave_try = time(NULL);

    // 调用fork()创建子进程,CHILD_TYPE_RDB标记为RDB持久化子进程
    if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) {
        int retval;

        /* Child 
         * 修改子进程标题以便监控(如ps命令显示redis-rdb-bgsave) */
        redisSetProcTitle("redis-rdb-bgsave");
        // 设置子进程CPU亲和性,避免子进程和主进程在同一CPU上执行。
        redisSetCpuAffinity(server.bgsave_cpulist);
        // 生成RDB文件
        retval = rdbSave(filename,rsi);
        if (retval == C_OK) {
            // 向父进程发送子进程执行RDB期间的COW内存使用情况
            sendChildCowInfo(CHILD_INFO_TYPE_RDB_COW_SIZE, "RDB");
        }
        exitFromChild((retval == C_OK) ? 0 : 1);	// 退出子进程
    } else {
        /* Parent */
        if (childpid == -1) {	// fork创建失败(很可能是内存不足)
            server.lastbgsave_status = C_ERR;	// 设置失败标志
            serverLog(LL_WARNING,"Can't save in background: fork: %s",
                strerror(errno));
            return C_ERR;
        }
        serverLog(LL_NOTICE,"Background saving started by pid %ld",(long) childpid);
        server.rdb_save_time_start = time(NULL);	// 更新最新save时间为当前时间
        // RDB_CHILD_TYPE_DISK表明RDB进程在写入磁盘文件
        // RDB_CHILD_TYPE_SOCKET表明RDB子进程在向slave发送RDB文件
        server.rdb_child_type = RDB_CHILD_TYPE_DISK;
        return C_OK;
    }
    return C_OK; /* unreached */
}

rdbSaveBackground 会调用 redisFork 创建 RDB 子进程。在子进程中也会调用 rdbSave 生成 RDB 文件(同 SAVE 命令)。

成功执行完 rdbSave 生成 RDB 文件后,会调用 sendChildCowInfo(CHILD_INFO_TYPE_RDB_COW_SIZE, "RDB") 向父进程发送子进程执行期间的 COW 内存占用情况。

void sendChildCowInfo(childInfoType info_type, char *pname) {
    sendChildInfoGeneric(info_type, 0, -1, pname);
}

继续调用 sendChildInfoGeneric()

/* 向父进程发送数据 */
void sendChildInfoGeneric(childInfoType info_type, size_t keys, double progress, char *pname) {
    // 通过管道向父进程发送数据。如果管道没打开,直接返回
    if (server.child_info_pipe[1] == -1) return;

    // 注意:3个静态变量
    static monotime cow_updated = 0;
    static uint64_t cow_update_cost = 0;
    static size_t cow = 0;

    child_info_data data = {0}; /* zero everything, including padding to satisfy valgrind */

    /* When called to report current info, we need to throttle down CoW updates as they
     * can be very expensive. To do that, we measure the time it takes to get a reading
     * and schedule the next reading to happen not before time*CHILD_COW_COST_FACTOR
     * passes. */

    monotime now = getMonotonicUs(); // 获取当前时间
    // 动态调整采样间隔,平衡监控精度与性能开销。
    // 例如,若某次统计耗时100ms,则下次统计至少延迟至100ms * 10 = 1s后执行
    if (info_type != CHILD_INFO_TYPE_CURRENT_INFO ||
        !cow_updated ||
        now - cow_updated > cow_update_cost * CHILD_COW_DUTY_CYCLE)
    {
        // 获取/proc/self/smaps中的"Private_Dirty"键值
        cow = zmalloc_get_private_dirty(-1);
        cow_updated = getMonotonicUs();	// 更新为当前时间
        cow_update_cost = cow_updated - now; // 计算 cow updated 耗时

        if (cow) {
            serverLog((info_type == CHILD_INFO_TYPE_CURRENT_INFO) ? LL_VERBOSE : LL_NOTICE,
                      "%s: %zu MB of memory used by copy-on-write",
                      pname, cow / (1024 * 1024));
        }
    }

    // 封装cow数据到data中
    data.information_type = info_type;
    data.keys = keys;
    data.cow = cow;
    data.cow_updated = cow_updated;
    data.progress = progress;

    ssize_t wlen = sizeof(data);
    // 通过管道发送data给父进程
    if (write(server.child_info_pipe[1], &data, wlen) != wlen) {
        /* Failed writing to parent, it could have been killed, exit. */
        serverLog(LL_WARNING,"Child failed reporting info to parent, exiting. %s", strerror(errno));
        exitFromChild(1);	// 退出子进程
    }
}

我们看下 zmalloc_get_private_dirty 函数

/* Return the total number bytes in pages marked as Private Dirty.
 *
 * Note: depending on the platform and memory footprint of the process, this
 * call can be slow, exceeding 1000ms!
 */
size_t zmalloc_get_private_dirty(long pid) {
    return zmalloc_get_smap_bytes_by_field("Private_Dirty:",pid);
}
size_t zmalloc_get_smap_bytes_by_field(char *field, long pid) {
    char line[1024];
    size_t bytes = 0;
    int flen = strlen(field);
    FILE *fp;

    // pid为-1,获取当前进程的内存映射信息
    if (pid == -1) {
        fp = fopen("/proc/self/smaps","r");	// 打开smaps文件
    } else {
        char filename[128];
        snprintf(filename,sizeof(filename),"/proc/%ld/smaps",pid);
        fp = fopen(filename,"r");
    }

    if (!fp) return 0;
    // 按行读取
    while(fgets(line,sizeof(line),fp) != NULL) {
        if (strncmp(line,field,flen) == 0) {
            char *p = strchr(line,'k');
            if (p) {
                *p = '\0';
                bytes += strtol(line+flen,NULL,10) * 1024;	// 累加所有的Private_Dirty值
            }
        }
    }
    fclose(fp);
    return bytes;
}

执行 cat /proc/self/smaps 查看 Private_Dirty 的信息。

$ cat /proc/self/smaps | grep Private_Dirty
Private_Dirty:         0 kB
Private_Dirty:         0 kB
Private_Dirty:         0 kB
Private_Dirty:         4 kB
Private_Dirty:         4 kB
Private_Dirty:        12 kB
Private_Dirty:         0 kB
Private_Dirty:         0 kB
Private_Dirty:         0 kB
Private_Dirty:         0 kB
Private_Dirty:         0 kB
Private_Dirty:        16 kB
Private_Dirty:         8 kB
Private_Dirty:        16 kB
Private_Dirty:        16 kB
Private_Dirty:         4 kB
Private_Dirty:         0 kB
Private_Dirty:         0 kB
Private_Dirty:         0 kB
Private_Dirty:         8 kB
Private_Dirty:         8 kB
Private_Dirty:        16 kB
Private_Dirty:         0 kB
Private_Dirty:         0 kB
Private_Dirty:         0 kB
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值