深入剖析Redis RDB持久化机制 (3)

其中REDIS_HASH_ZIPMAP,REDIS_LIST_ZIPLIST,REDIS_SET_INTSET和REDIS_ZSET_ZIPLIST这四种数据类型都是只在rdb文件中才有的类型,其他的数据类型其实就是val对象中type字段存储的值。

下边以REDIS_STRING类型和REDIS_LIST类型为例进行详解,其他类型都类似

REDIS_STRING类型

假设rdb文件中有一个值是REDIS_STRING类型,比如执行了一个set mykey myval命令,则在rdb文件表示为:

REDIS_STRING类型 | 值

其中值包含了key的长度,key的值,val的长度和val的值,把REDIS_STRING类型值的格式代入得:

REDIS_STRING类型 | keylen | mykey | vallen | myval

长度的存储格式见rdb中长度的存储

REDIS_LIST类型

1.List

REDIS_LIST | listlen | len | value | len | value

Listlen是链表长度

Len是链表结点的值value的长度

Value是链表结点的值

2.Ziplist

REDIS_ENCODING_ZIPLIST | ziplist

Ziplist就是通过字符串来实现的,直接将其存储于rdb文件中即可

快照保存

我们接下来看看具体实现细节

不管是触发条件满足后通过fork子进程来保存快照还是通过save命令来触发,其实都是调用的同一个函数rdbSave(rdb.c:394)。

先来看看触发条件满足后通过fork子进程的实现保存快照的的实现

在每100ms调用一次的serverCron函数中会对快照保存的条件进行检查,如果满足了则进行快照保存

Redis.c:604

/* Check if a background saving or AOF rewrite in progress terminated */ if (server.bgsavechildpid != -1 || server.bgrewritechildpid != -1) { int statloc; pid_t pid; if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) { if (pid == server.bgsavechildpid) { backgroundSaveDoneHandler(statloc); } … updateDictResizePolicy(); } } else { time_t now = time(NULL); /* If there is not a background saving in progress check if * we have to save now */ for (j = 0; j < server.saveparamslen; j++) { struct saveparam *sp = server.saveparams+j; if (server.dirty >= sp->changes && now-server.lastsave > sp->seconds) { redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving…", sp->changes, sp->seconds); rdbSaveBackground(server.dbfilename); break; } } … }

如果后端有写rdb的子进程或者写aof的子进程,则检查rdb子进程是否退出了,如果退出了则进行一些收尾处理,比如更新脏数据计数server.dirty和最近快照保存时间server.lastsave。

如果后端没有写rdb的子进程且没有写aof的子进程,则判断下是否有触发写rdb的条件满足了,如果有条件满足,则通过调用rdbSaveBackground函数进行快照保存。

跟着进rdbSaveBackground函数里边看看

Rdb.c:499

int rdbSaveBackground(char *filename) { pid_t childpid; long long start; if (server.bgsavechildpid != -1) return REDIS_ERR; if (server.vm_enabled) waitEmptyIOJobsQueue(); server.dirty_before_bgsave = server.dirty; start = ustime(); if ((childpid = fork()) == 0) { /* Child */ if (server.vm_enabled) vmReopenSwapFile(); if (server.ipfd > 0) close(server.ipfd); if (server.sofd > 0) close(server.sofd); if (rdbSave(filename) == REDIS_OK) { _exit(0); } else { _exit(1); } } else { /* Parent */ server.stat_fork_time = ustime()-start; if (childpid == -1) { redisLog(REDIS_WARNING,"Can\'t save in background: fork: %s", strerror(errno)); return REDIS_ERR; } redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid); server.bgsavechildpid = childpid; updateDictResizePolicy(); return REDIS_OK; } return REDIS_OK; /* unreached */ }

对是否已经有写rdb的子进程进行了判断,如果已经有保存快照的子进程,则返回错误。

如果启动了虚拟内存,则等待所有处理换出换入的任务线程退出,如果还有vm任务在处理就会一直循环等待。一直到所有换入换出任务都完成且所有vm线程退出。

保存当前的脏数据计数,当快照保存完后用于更新当前的脏数据计数(见函数backgroundSaveDoneHandler,rdb.c:1062)

记下当前时间,用于统计fork一个进程需要的时间

Fork一个字进程,子进程调用rdbSave进行快照保存

父进程统计fork一个子进程消耗的时间: server.stat_fork_time = ustime()-start,这个统计可以通过info命令获得。

保存子进程ID和更新增量重哈希的策略,即此时不应该再进行增量重哈希,不然大量key的改变可能导致fork的copy-on-write进行大量的写。

到了这里我们知道,rdb的快照保存是通过函数rdbSave函数(rdb.c:394)来实现的。其实save命令也是通过调用这个函数来实现的。我们来简单看看

Db.c:323

void saveCommand(redisClient *c) { if (server.bgsavechildpid != -1) { addReplyError(c,"Background save already in progress"); return; } if (rdbSave(server.dbfilename) == REDIS_OK) { addReply(c,shared.ok); } else { addReply(c,shared.err); }

最后我们进rdbSave函数看看

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzzddp.html