Redis SNAPSHOT
上一篇文章我們學習了redis aof的實現.這篇文章我們將學習redis的另一種持久化方式:snapshot(快照)。
同上一篇文章一樣,我們首先介紹相關參數;然後依次介紹它的使用情境。
1. 配置參數
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; } }
這裡我們不再介紹rdbSaveBackground函數,而直接介紹子進程的處理rdbSave:
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); ...}
該函數相對於aof的rewriteAppendOnlyFile簡單得多,裡面的注釋也很清楚,關於value的type與encode可參考:http://www.w3ccollege.org/redis/redis-internal/redis-memory-storage-structure-analysis-2.html
當後檯子進程結束的時候,主線程在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); }}
註:該方式是在主線程中執行save rdb操作,所以會阻塞主線程的工作。所以一般不建議使用該命令,而使用下面的命令。
B.bgsave
該命令的回呼函數為:bgsaveCommand,該函數也是通過調用rdbSaveBackground來實現,即其實這個過程跟上面的server判斷時間改變條件的過程是一樣,只是後者是由用戶端通過命令來強制它執行,而不是像前者那樣等到條件滿足時才執行。所以我們這裡也不再贅述。
3.總結
redis的SNAPSHOT有三種應用情境:其一在每次運行serverCron的時候去檢測時間改變條件是否滿足,如果滿足就會建立一個後檯子進程進行記憶體資料到dump.rdb檔案的寫過程:將記憶體資料寫到一個tempfile,然後再rename(也就是每次都是把整個記憶體資料儲存起來,而不是修改更新的資料);用戶端發送save命令,此時是在主線程中執行的,所以會阻塞主線對file event的響應;最後一種跟第一種很相似只是它是通過用戶端發送bgsave來實現。另外之所以稱為SNAPSHOT,是因為它是利用fork子進程與父進程是通過copy-on-write的方式來暫時共用記憶體位址空間的,而不影響父進程。