redis rdb持久化源码分析

Posted bruce128

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了redis rdb持久化源码分析相关的知识,希望对你有一定的参考价值。

毕业后的7年一直是Java选手,第一次这么认真的看C写的源码,本科学的C好多东西忘了。本文基于redis 5.0.7的源码分析被动方式的rdb持久化。

一、 redis的持久化方式

redis是支持持久化的内存数据库(memcached不支持持久化)。其持久化的方式有两种,aof和rdb。rdb是一种快照式(snapshot)的持久化,直接把redis的内存整体写入磁盘文件。触发rdb持久化有两种方式,直接客户端调用bgsave命令或者固定时间内的写命令达到配置文件里的触发规则。

rdb持久化规则配置:

#   save <seconds> <changes>
#
#   Will save the DB if both the given number of seconds and the given
#   number of write operations against the DB occurred.
save 900 1
save 300 10
save 60 10000

二、RDB持久化入口

rdb的持久化大致流程如下

rdb的自动间隔保存由serverCron函数触发,满足配置的自动保存条件则执行后台保存。
触发逻辑,对应代码在server.c里的serverCron函数里

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) 
... ...

	/* 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
	     * CONFIG_BGSAVE_RETRY_DELAY seconds already elapsed. */
	     // dirty存储的是自从上次持久化后未被保存的写操作数量
	     // 判断是否达到自动保存条件条件
	    if (server.dirty >= sp->changes &&
	        server.unixtime-server.lastsave > sp->seconds &&
	        (server.unixtime-server.lastbgsave_try >
	         CONFIG_BGSAVE_RETRY_DELAY ||
	         server.lastbgsave_status == C_OK))
	    
	        serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...",
	            sp->changes, (int)sp->seconds);
	        rdbSaveInfo rsi, *rsiptr;
	        rsiptr = rdbPopulateSaveInfo(&rsi);
	        // 开启新进程执行dump操作
	        rdbSaveBackground(server.rdb_filename,rsiptr);
	        break;
	    
	
... ... 

saveparam里存储的是自动保存的条件

struct saveparam 
    time_t seconds; 
    int changes;
;

结合rdb持久化的条件考虑如下场景

机器突然宕机,此时有多个新的写操作尚未落入磁盘,那么丢失的数据将是永久的信息湮灭

三、创建后台进程逻辑

redis会通过fork()系统调用新开一个进程执行保存逻辑。逻辑在rdb.c文件里的rdbSaveBackground函数里。

int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) 
    if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;

    server.dirty_before_bgsave = server.dirty;
    server.lastbgsave_try = time(NULL);
    // 开启父子进程通信管道
    openChildInfoPipe();

    start = ustime();
    // 子进程执行持久化逻辑
    if ((childpid = fork()) == 0) 
        int retval;

        // 关闭负责监听客户端连接的套接字,避免客户端连接到这个进程
        closeListeningSockets(0);
        redisSetProcTitle("redis-rdb-bgsave");
        // rdb持久化入口
        retval = rdbSave(filename,rsi);
        if (retval == C_OK) 
            size_t private_dirty = zmalloc_get_private_dirty(-1);

            if (private_dirty) 
                serverLog(LL_NOTICE,
                    "RDB: %zu MB of memory used by copy-on-write",
                    private_dirty/(1024*1024));
            

            server.child_info_data.cow_size = private_dirty;
            // 通过管道向父进程发送持久化的统计数据
            sendChildInfo(CHILD_INFO_TYPE_RDB);
        
        // 子进程退出
        exitFromChild((retval == C_OK) ? 0 : 1);
     else 
        /* Parent 父进程 */
      	// 父进程记录统计信息
        server.stat_fork_time = ustime()-start;
        server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
        latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
        if (childpid == -1) 
            closeChildInfoPipe();
            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 %d",childpid);
        // 记录持久化进程信息
        server.rdb_save_time_start = time(NULL);
        server.rdb_child_pid = childpid;
        server.rdb_child_type = RDB_CHILD_TYPE_DISK;
        updateDictResizePolicy();
        return C_OK;
        // 当前时间事件到此结束,aemain会开启下一个事件(文件事件或事件事件)
    
    return C_OK; /* unreached */

这里有两个核心的函数,确切的说是系统调用,fork和exit。fork会创建一个子进程,而父进程运行到wait函数之后会阻塞自己直到子进程调用exit结束自己的生命周期。关于fork函数的返回值请大家自行Google。

四、后台保存逻辑

保存逻辑由rdb.c文件rdbSave函数和rdbSaveRio函数执行

/* Save the DB on disk. Return C_ERR on error, C_OK on success. */
int rdbSave(char *filename, rdbSaveInfo *rsi) 
	// 创建保存数据库的临时文件
    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    fp = fopen(tmpfile,"w");

	// 初始化redis自己封装的io工具
    rioInitWithFile(&rdb,fp);

    if (server.rdb_save_incremental_fsync)
        riosetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES);

	// 同步数据
    if (rdbSaveRio(&rdb,&error,RDB_SAVE_NONE,rsi) == C_ERR) 
        errno = error;
        goto werr;
    

    /* Make sure data will not remain on the OS's output buffers */
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;

    /* Use RENAME to make sure the DB file is changed atomically only
     * if the generate DB file is ok. */
    // 对临时文件改名,扶正,rename函数确保原子性
    if (rename(tmpfile,filename) == -1) 
        char *cwdp = getcwd(cwd,MAXPATHLEN);
        // 对临时文件的连接数进行减一操作,如果连接数减到了0,则删除
        unlink(tmpfile);
        return C_ERR;
    

    serverLog(LL_NOTICE,"DB saved on disk");
    // 保存好后,没有同步到磁盘的写操作数则为0
    server.dirty = 0;
    server.lastsave = time(NULL);
    server.lastbgsave_status = C_OK;
    return C_OK;

rdbSaveRio函数会将redis的所有数据库里的所有键值对写入磁盘,生成一个合乎rdb文件规范的dump。源码不难,这里不贴粗来了。

五、父进程的等待机制

父进程在调用了fork执行后台保存rdb后,随即执行下一个事件。父进程会调用wait3函数等待子进程结束,这个过程是阻塞的。那么意味着redis的数据量越大,父进程无法提供服务的时间越长。

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 ||
        ldbPendingChildren())
    
    	// 阻塞直到子进程结束
        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) 
            	// 子进程结束后,调用处理函数,删除临时文件,在server的数据结构里标记进程结束
                backgroundSaveDoneHandler(exitcode,bysignal);
                if (!bysignal && exitcode == 0) receiveChildInfo();
             else if (pid == server.aof_child_pid) 
                backgroundRewriteDoneHandler(exitcode,bysignal);
                if (!bysignal && exitcode == 0) receiveChildInfo();
             
            updateDictResizePolicy();
            // 关闭通信管道
            closeChildInfoPipe();
        
    

子进程在dump结束后,会将未持久化的变量(dirty)置为0。如果父进程在子进程尚未结束的时候就接受IO操作请求,那么必将污染这个变量,同时新增的IO写操作可能部分写入磁盘,部分没写入磁盘。数据不一致,会导致灾难。因此父进程会一直等到子进程结束再接收新的请求。

小结:RDB方式的缺陷

根据上述对源码的分析,rdb方式有两个缺点

  1. 持久化期间,redis服务停摆。停摆的时间和Redis的内存使用量成正比
  2. 一旦宕机,rdb可能会丢失数据。

redis官方文档 有提到这两个问题

RDB disadvantages
RDB is NOT good if you need to minimize the chance of data loss in case Redis stops working (for example after a power outage). You can configure different save points where an RDB is produced (for instance after at least five minutes and 100 writes against the data set, but you can have multiple save points). However you’ll usually create an RDB snapshot every five minutes or more, so in case of Redis stopping working without a correct shutdown for any reason you should be prepared to lose the latest minutes of data.
RDB needs to fork() often in order to persist on disk using a child process. Fork() can be time consuming if the dataset is big, and may result in Redis to stop serving clients for some millisecond or even for one second if the dataset is very big and the CPU performance not great. AOF also needs to fork() but you can tune how often you want to rewrite your logs without any trade-off on durability.

附录:如何调试redis


我的环境是mac,Mac下没有VS那么强悍的C语言IDE。由于redis使用了linux的系统库,windows下是没法编译redis源码的。
推荐一个使用xcode调试redis源码的帖子: https://blog.csdn.net/u011577874/article/details/73000207

以上是关于redis rdb持久化源码分析的主要内容,如果未能解决你的问题,请参考以下文章

redis源码分析--aof持久化

redis RDB 和AOF

Redis源码剖析 - Redis持久化之RDB

Redis源码剖析 - Redis持久化之RDB

Redis源码剖析 - Redis持久化之RDB

阅读redis持久化RDB源码的时候一些c知识