Redis SNAPSHOT
上一篇文章我们学习了redis aof的实现.这篇文章我们将学习redis的另一种持久化方式:snapshot(快照)。
同上一篇文章一样,我们首先介绍相关参数;然后依次介绍它的使用场景。
save <seconds> <changes>:相对一个DB,多少秒内发生了多少次更新操作,此时就会进行一次保存操作,这个可以设置多个条件,它们中的任一个满足都会保存一次,下面把这个配置叫做时间变化条件。
rdbcompression yes:是否进行压缩操作
dbfilename dump.rdb:数据文件的名字
dir /u01/xiangzhong/redis-2.4.2/data:上面文件保存的位置
2.使用场景
通过上面的参数save <seconds> <changes>我们可以知道当触发这个条件的时候就会执行一次save rdb操作。所以我们先来看一下这个过程。
2.1 save <seconds> <changes>条件触发自动运行
该过程在serverCron里判断
- /* If there is not a background saving in progress check if
- * we have to save now */
- for (j = 0; j < server.saveparamslen; j++) {//判断总共有多少个时间变化条件,这个总共由三部分组成,系统启动时initServerConfig初始化三个[(60*60,1),(300,100),(60,10000)];然后就是加载配置文件loadServerConfig;最后就是client通过命令来添加条件;它们被保存到server.saveparams这个动态数组里,在追加的时候并没有对覆盖情况进行去重,不过这个数据不会太大,所以这个影响不大
- 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;
- }
- }
- int rdbSave(char *filename) {
- ...
- snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid()); //生成一个新的临时文件
- fp = fopen(tmpfile,"w");
- if (fwrite("REDIS0002",9,1,fp) == 0) goto werr;
- for (j = 0; j < server.dbnum; j++) {
- redisDb *db = server.db+j;
- dict *d = db->dict;
- if (dictSize(d) == 0) continue;
- di = dictGetSafeIterator(d);
- //写select db 命令,只是这里使用的是编码的方式,而不是可读的字符命令
- if (rdbSaveType(fp,REDIS_SELECTDB) == -1) goto werr;
- if (rdbSaveLen(fp,j) == -1) goto werr;//db id
- while((de = dictNext(di)) != NULL) { //遍历该db内的所有表的所有记录entry
- sds keystr = dictGetEntryKey(de); //获得key值
- robj key, *o = dictGetEntryVal(de); //获得value
- time_t expiretime;
- initStaticStringObject(key,keystr); //key有两种可能encode:RAW,INT这里直接使用RAW,只有是数据的才可能被encode为INT,所以RAW是安全,可靠的
- expiretime = getExpire(db,&key);
- if (expiretime != -1) {
- //判断该key是否已经过期,如果过期了就不保存了直接跳过
- if (expiretime < now) continue;
- if (rdbSaveType(fp,REDIS_EXPIRETIME) == -1) goto werr;
- if (rdbSaveTime(fp,expiretime) == -1) goto werr;
- }
- if (!server.vm_enabled || o->storage == REDIS_VM_MEMORY ||
- o->storage == REDIS_VM_SWAPPING) {
- int otype = getObjectSaveType(o); //获得value的类型
- /* Save type, key, value */
- if (rdbSaveType(fp,otype) == -1) goto werr;
- if (rdbSaveStringObject(fp,&key) == -1) goto werr;
- if (rdbSaveObject(fp,o) == -1) goto werr;
- }
- ...
- } //end while dict entry
- dictReleaseIterator(di);
- } //end for db
- if (rdbSaveType(fp,REDIS_EOF) == -1) goto werr;
- fflush(fp);
- fsync(fileno(fp));
- fclose(fp);
- rename(tmpfile,filename);
- server.dirty = 0; //对于子进程这两个值更新没有作用,但如果是使用save命令,则它是在主线程里进行所以必须更新
- server.lastsave = time(NULL);
- ...
- }
当后台子进程结束的时候,主线程在serverCron里wait该信号(这与aof的时机一样),我们这里直接看一下信号处理函数backgroundSaveDoneHandler
- server.dirty = server.dirty - server.dirty_before_bgsave; //更新dirty
- server.lastsave = time(NULL);
- server.bgsavechildpid = -1;
- updateSlavesWaitingBgsave(exitcode == 0 ? REDIS_OK : REDIS_ERR); //这个与repl slave有关,暂不介绍
2.2客户端命令触发
上面我们介绍了系统自动检测时间改变条件,完成save rdb的过程,下面我们将介绍另外两种情况它们都是由客户端通过命令来触发的:save、bgsave。
A.save
该命令的回调函数为:saveCommand
- void saveCommand(redisClient *c) { //如果现在后面刚好有一个进程在save,显然不再需要了,另外该函数也是通过调用rdbSave来实现的。
- 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);
- }
- }
B.bgsave
该命令的回调函数为:bgsaveCommand,该函数也是通过调用rdbSaveBackground来实现,即其实这个过程跟上面的server判断时间改变条件的过程是一样,只是后者是由客户端通过命令来强制它执行,而不是像前者那样等到条件满足时才执行。所以我们这里也不再赘述。
3.总结
redis的SNAPSHOT有三种应用场景:其一在每次运行serverCron的时候去检测时间改变条件是否满足,如果满足就会创建一个后台子进程进行内存数据到dump.rdb文件的写过程:将内存数据写到一个tempfile,然后再rename(也就是每次都是把整个内存数据保存起来,而不是修改更新的数据);客户端发送save命令,此时是在主线程中执行的,所以会阻塞主线对file event的响应;最后一种跟第一种很相似只是它是通过客户端发送bgsave来实现。另外之所以称为SNAPSHOT,是因为它是利用fork子进程与父进程是通过copy-on-write的方式来暂时共享内存地址空间的,而不影响父进程。