众所周知,redis RDB持久化主要有两种方式:save和bgsave,其触发方式包括自动和手动。bgsave不论在何种触发方式下均通过后台进程的形式进行,对主线程阻塞相对很短,生产环境下推荐使用;而save命令会阻塞主线程直到持久化完成,生产环境下一般禁止使用。而在redis服务的redis.conf配置文件中,有关于RDB持久化,是通过“save <seconds> <changes>”的形式进行配置,问题来了,这个save是对应的save命令还是bgsave命令?这种配置形式是不是可能让人产生误解?
以redis-2.8.24源码为例,先来校验下两种命令格式:
struct redisCommand redisCommandTable[] = { ... {"save",saveCommand,1,"ars",0,NULL,0,0,0,0,0}, {"bgsave",bgsaveCommand,1,"ar",0,NULL,0,0,0,0,0}, ... };
由上面的redisCommandTable全局数组可知,save和bgsave分别对应saveCommand和bgsaveCommand,这两个命令可以望文生义。
rdb持久化的两个命令在rdb.c中实现,saveCommand实质阻塞调用rdbSave来完成,而bgsaveCommand则通过rdbSaveBackground内部fork子进程来执行rdbSave,不再贴出源码,符合逻辑,没有问题。
那我们来看redis.conf文件的读取加载,redis服务的入口main函数中调用loadServerConfig完成redis.conf的文件读取(config.c),内部调用loadServerConfigFromString:
void loadServerConfigFromString(char *config) { ... } else if (!strcasecmp(argv[0],"save")) { if (argc == 3) { int seconds = atoi(argv[1]); int changes = atoi(argv[2]); if (seconds < 1 || changes < 0) { err = "Invalid save parameters"; goto loaderr; } appendServerSaveParams(seconds,changes); } else if (argc == 2 && !strcasecmp(argv[1],"")) { resetServerSaveParams(); } } ... } void appendServerSaveParams(time_t seconds, int changes) { server.saveparams = zrealloc(server.saveparams,sizeof(struct saveparam)*(server.saveparamslen+1)); server.saveparams[server.saveparamslen].seconds = seconds; server.saveparams[server.saveparamslen].changes = changes; server.saveparamslen++; }
读取配置文件中关于RDB持久化的配置,初始化全局的redisServer结构体。
继续跟踪redis-server执行的serverCron事件(redis.c),这里完成对redis.conf中的save配置的触发执行:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { ... /* Check if a background saving or AOF rewrite in progress terminated. */ if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) { int statloc; pid_t pid; if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) { int exitcode = WEXITSTATUS(statloc); int bysignal = 0; if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc); if (pid == server.rdb_child_pid) { backgroundSaveDoneHandler(exitcode,bysignal); } else if (pid == server.aof_child_pid) { backgroundRewriteDoneHandler(exitcode,bysignal); } else { redisLog(REDIS_WARNING, "Warning, detected child with unmatched pid: %ld", (long)pid); } updateDictResizePolicy(); } } else { /* If there is not a background saving/rewrite in progress check if * we have to save/rewrite now */ for (j = 0; j < server.saveparamslen; j++) { struct saveparam *sp = server.saveparams+j; /* Save if we reached the given amount of changes, * the given amount of seconds, and if the latest bgsave was * successful or if, in case of an error, at least * REDIS_BGSAVE_RETRY_DELAY seconds already elapsed. */ if (server.dirty >= sp->changes && server.unixtime-server.lastsave > sp->seconds && (server.unixtime-server.lastbgsave_try > REDIS_BGSAVE_RETRY_DELAY || server.lastbgsave_status == REDIS_OK)) { redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving...", sp->changes, (int)sp->seconds); rdbSaveBackground(server.rdb_filename); break; } } /* Trigger an AOF rewrite if needed */ if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 && server.aof_rewrite_perc && server.aof_current_size > server.aof_rewrite_min_size) { long long base = server.aof_rewrite_base_size ? server.aof_rewrite_base_size : 1; long long growth = (server.aof_current_size*100/base) - 100; if (growth >= server.aof_rewrite_perc) { redisLog(REDIS_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth); rewriteAppendOnlyFileBackground(); } } } ... }
由第36-46可知,达到redis.conf中配置的save触发条件,会调用rdbSaveBackground,可见,这个自动触发的save配置,实际是对应后台进程的bgsave调用。
对于开篇提出的问题,疑惑终于解开。对于redis.conf的save配置,此save非彼save,笔者觉得redis.conf配置中不如就改成bgsave,或者整个redis就直接把阻塞的save完全废弃掉吧。