当前位置: 首页 > news >正文

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) {// 已经存在后台子进程在执行RDBif (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

http://www.mrgr.cn/news/99207.html

相关文章:

  • Linux系统下docker 安装 MySQL
  • 深入理解 TCP 协议 | 流量、拥塞及错误控制机制
  • MATLAB 控制系统设计与仿真 - 37
  • javascript day4
  • 深度学习算法:从基础到实践
  • MFC文件-写MP4
  • 【更新中】【k8s系列6】RKE搭建Kubernetes集群
  • UE5 渲染视频
  • Golang errors 包快速上手
  • CentOS更换yum源
  • 红队专题-漏洞挖掘-代码审计-反序列化
  • 网络编程 - 4 ( TCP )
  • git提交实现文件或目录忽略
  • ZYNQ-GPIO外设
  • 从单模态到多模态:大模型架构演进与技术介绍
  • 【adb】bat批处理+adb 自动亮屏,自动解锁屏幕,启动王者荣耀
  • 前端路由缓存实现
  • Flutter之资源和媒体
  • springboot3 cloud gateway 配置websocket代理转发教程
  • prism