標籤:redis c
RDB是redis的另一種持久化方式,相當於是定時快照,也用於主從同步中快照+redo log。redis在進行RDB時,不需要加鎖,這是通過利用父子進程共用同一份記憶體完成的。在父進程fork子進程之後,父子以copy-on-write方式共用同一份實體記憶體,當兩個進程寫記憶體時,才會按照記憶體頁複製記憶體。這就需要保證在RDB時,最壞情況下需要保證有2倍的記憶體空間用於父子進程使用(redis使用時佔用1G,那麼就要保證系統有2G的記憶體,否則可能會出現使用swap的情況)。因為copy-on-write,所以需要避免不必要的記憶體拷貝。子進程中基本上只需要讀記憶體,而父進程響應用戶端請求,就需要修改記憶體,為了減少記憶體修改,父進程會暫停keyspace對應的hash表的rehash(rehash會有大量拷貝,要在不同的桶之間拷貝資料)。下面看一下RDB相關內容。
1. RDB檔案格式
RDB檔案格式比較簡單,可以看做是一條條指令序列,每條指令的組成:
|-----------------------|----------------------------------|
| OP code: 1Byte | Instruction: nBytes |
|-----------------------|----------------------------------|
在載入RDB時,就是對這個指令序列進行解析。所有的OP code包括:
REDIS_RDB_OPCODE_EXPIRETIME_MS: ms級的到期時間
REDIS_RDB_OPCODE_EXPIRETIME:秒級的到期時間
REDIS_RDB_OPCODE_SELECTDB:用於select db命令
REDIS_RDB_OPCODE_EOF:RDB檔案結尾
OP code還包括所有的資料類型(REDIS_RDB_TYPE_LIST, REDIS_RDB_TYPE_SET等),用於指定後續kv對中,value的類型。
RDB檔案的前5個位元組是magic number,用於表示檔案是RDB檔案。接下來的4個位元組是版本號碼,在載入RDB時,會根據RDB的版本號碼和redis的版本號碼比較,查看是否可以處理該版本的RDB。然後,是一條條指令序列,最後以EOF結尾。
2. RDB dump
首先看一下dump的時機,主要分為3塊:
1)save命令:用戶端發送save命令,redis執行個體阻塞執行dump。在saveCommand函數中。
2)bgsave命令:dump任務由子進程完成,主進程可以繼續服務要求。在bgsaveCommand函數中。
3)被動觸發:redis變更次數或者dump的間隔超過閾值。在serverCron中,檢測並觸發。
4)主從同步觸發:在不能實現partial sync時,master需要將rdb傳輸給slave。在syncCommand函數中。
下面看一下rdb dump的具體過程,這是由rdbSave函數完成的。
// <MM> // 建立並開啟臨時rdb檔案 // </MM> snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid()); fp = fopen(tmpfile,"w"); if (!fp) { redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s", strerror(errno)); return REDIS_ERR; } rioInitWithFile(&rdb,fp); if (server.rdb_checksum) rdb.update_cksum = rioGenericUpdateChecksum;建立並開啟臨時檔案,這是為了保證rdb的資料完整性,只有在dump成功後,才會替換原檔案。然後是初始化rio,用於輸出。
// <MM> // 寫入magic number,format: // 9bit: REDIS[RDB_VERSION] // </MM> snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION); if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;
寫入magic number以及版本號碼。
接下來是一個迴圈,用於對每個redis DB遍曆,並產生對應的內容。
for (j = 0; j < server.dbnum; j++) { // dump該DB }看下每個DB的dump過程,實際上就是遍曆並輸出每個Key-Value對。
redisDb *db = server.db+j; dict *d = db->dict; if (dictSize(d) == 0) continue; di = dictGetSafeIterator(d); if (!di) { fclose(fp); return REDIS_ERR; }先判斷DB是否為空白,如果為空白就跳過。然後擷取DB的迭代器。
/* Write the SELECT DB opcode */ if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr; if (rdbSaveLen(&rdb,j) == -1) goto werr;
輸出select db的opcode,然後是對應的DB號。具體格式是,1位元組的OPcode,加上1,2或4位元組的DB號。
/* Iterate this DB writing every entry */ while((de = dictNext(di)) != NULL) { sds keystr = dictGetKey(de); robj key, *o = dictGetVal(de); long long expire; initStaticStringObject(key,keystr); expire = getExpire(db,&key); if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr; } dictReleaseIterator(di);然後是一個while迴圈,遍曆所有K-V對,並進行dump。對於每個KV,擷取key,value和expire time。然後調用rdbSaveKeyValuePair函數進行dump,下面就看一下這個函數。
/* Save a key-value pair, with expire time, type, key, value. * On error -1 is returned. * On success if the key was actually saved 1 is returned, otherwise 0 * is returned (the key was already expired). */int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val, long long expiretime, long long now){ /* Save the expire time */ if (expiretime != -1) { /* If this key is already expired skip it */ if (expiretime < now) return 0; if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EXPIRETIME_MS) == -1) return -1; if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1; } /* Save type, key, value */ if (rdbSaveObjectType(rdb,val) == -1) return -1; if (rdbSaveStringObject(rdb,key) == -1) return -1; if (rdbSaveObject(rdb,val) == -1) return -1; return 1;}如果expire不為空白,則輸出該資訊。先寫出OPCode表示expire,然後是具體的逾時時間。接下來是具體的KV對,首先也類似OPCode,表示value的類型,然後是字串類型的key,最後是value對象。具體對象的dump內容比較多,這裡暫時不展開。
上面完成所有DB的dump後,接下來看一下收尾工作。
di = NULL; /* So that we don‘t release it again on error. */ /* EOF opcode */ if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;
輸出EOF對應的OPCode,表示rdb結束。
/* CRC64 checksum. It will be zero if checksum computation is disabled, the * loading code skips the check in this case. */ cksum = rdb.cksum; memrev64ifbe(&cksum); if (rioWrite(&rdb,&cksum,8) == 0) goto werr;
計算並輸出CRC。
/* 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;
首先是調用fflush將輸出緩衝區重新整理到page cache,然後調用fsync將cache中的內容寫盤,最後關閉檔案。
/* Use RENAME to make sure the DB file is changed atomically only * if the generate DB file is ok. */ if (rename(tmpfile,filename) == -1) { redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno)); unlink(tmpfile); return REDIS_ERR; }將臨時檔案重新命名為指定的檔案名稱。
redisLog(REDIS_NOTICE,"DB saved on disk"); server.dirty = 0; server.lastsave = time(NULL); server.lastbgsave_status = REDIS_OK; return REDIS_OK;
最後,列印日誌,重設dirty和lastsave,這兩個值會影響被動觸發rdb dump的時機。
werr: fclose(fp); unlink(tmpfile); redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno)); if (di) dictReleaseIterator(di); return REDIS_ERR;
上述出錯的錯誤處理,主要就是刪除臨時檔案,銷毀迭代器並列印日誌。
上面就是整個rdb dump的過程,在後台進行rdb dump時,上述是在子進程中完成的,主進程還需要進行最後的一些清理工作,下面看一下這個部分。在serverCron中,如果server.rdb_child_pid不為-1(存在rdb dump的子進程),會調用wait3對子進程收割,如果是rdb子進程完成,會調用backgroundSaveDoneHandler函數做最後處理。
/* A background saving child (BGSAVE) terminated its work. Handle this. */void backgroundSaveDoneHandler(int exitcode, int bysignal) { if (!bysignal && exitcode == 0) { redisLog(REDIS_NOTICE, "Background saving terminated with success"); server.dirty = server.dirty - server.dirty_before_bgsave; server.lastsave = time(NULL); server.lastbgsave_status = REDIS_OK; } else if (!bysignal && exitcode != 0) { redisLog(REDIS_WARNING, "Background saving error"); server.lastbgsave_status = REDIS_ERR; } else { mstime_t latency; redisLog(REDIS_WARNING, "Background saving terminated by signal %d", bysignal); latencyStartMonitor(latency); rdbRemoveTempFile(server.rdb_child_pid); latencyEndMonitor(latency); latencyAddSampleIfNeeded("rdb-unlink-temp-file",latency); /* SIGUSR1 is whitelisted, so we have a way to kill a child without * tirggering an error conditon. */ if (bysignal != SIGUSR1) server.lastbgsave_status = REDIS_ERR; } server.rdb_child_pid = -1; server.rdb_save_time_last = time(NULL)-server.rdb_save_time_start; server.rdb_save_time_start = -1; /* Possibly there are slaves waiting for a BGSAVE in order to be served * (the first stage of SYNC is a bulk transfer of dump.rdb) */ updateSlavesWaitingBgsave((!bysignal && exitcode == 0) ? REDIS_OK : REDIS_ERR);}相對於aof rewrite,這塊工作要簡單一些,主要是根據子進程的退出狀態以及是否被訊號kill進行處理。最後一個函數updateSlavesWaitingBgsave是用於在主從同步中,完成rdb dump,通知向slave傳輸rdb。
3. RDB Load
RDB載入主要是兩個地方會用到:
1)redis啟動時,載入RDB
2)主從同步時,master向從發送RDB
redis啟動時載入RDB,和AOF一樣,是在loadDataFromDisk函數:
/* Function called at startup to load RDB or AOF file in memory. */void loadDataFromDisk(void) { long long start = ustime(); if (server.aof_state == REDIS_AOF_ON) { if (loadAppendOnlyFile(server.aof_filename) == REDIS_OK) redisLog(REDIS_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000); } else { if (rdbLoad(server.rdb_filename) == REDIS_OK) { redisLog(REDIS_NOTICE,"DB loaded from disk: %.3f seconds", (float)(ustime()-start)/1000000); } else if (errno != ENOENT) { redisLog(REDIS_WARNING,"Fatal error loading the DB: %s. Exiting.",strerror(errno)); exit(1); } }}如果沒有開啟AOF配置,那麼就會嘗試載入RDB。由rdbLoad完成RDB的載入,下面看一下這個函數。
uint32_t dbid; int type, rdbver; redisDb *db = server.db+0; char buf[1024]; long long expiretime, now = mstime(); FILE *fp; rio rdb; if ((fp = fopen(filename,"r")) == NULL) return REDIS_ERR;
首先是開啟RDB檔案。
rioInitWithFile(&rdb,fp); rdb.update_cksum = rdbLoadProgressCallback; rdb.max_processing_chunk = server.loading_process_events_interval_bytes;
初始化rio,設定update_cksum回呼函數,以及read的塊大小(預設配置的是2M)。這裡使用update_cksum還完成一些功能:
1)更新載入進度
2)如果是主從同步過程中,載入RDB,因為整個載入過程可能會很漫長,所以需要不停的想master發送心跳,避免master認為這個slave已經timeout,主動中斷連線。
3)處理一些io事件
if (rioRead(&rdb,buf,9) == 0) goto eoferr; buf[9] = ‘\0‘; if (memcmp(buf,"REDIS",5) != 0) { fclose(fp); redisLog(REDIS_WARNING,"Wrong signature trying to load DB from file"); errno = EINVAL; return REDIS_ERR; } rdbver = atoi(buf+5); if (rdbver < 1 || rdbver > REDIS_RDB_VERSION) { fclose(fp); redisLog(REDIS_WARNING,"Can‘t handle RDB format version %d",rdbver); errno = EINVAL; return REDIS_ERR; }讀取RDB前9個位元組,校正magic number以及版本號碼。
startLoading(fp);
準備開始載入,記錄載入開始時間,以及需要載入的位元組總數,用於更新載入進度。
接下來是一個解釋迴圈,不停的讀取一條條指令。
while (1) { // 解釋一條條指令 }下面看一下一條指令的解釋過程:
robj *key, *val; expiretime = -1; /* Read type. */ if ((type = rdbLoadType(&rdb)) == -1) goto eoferr;
首先讀取type(對應OPCode)。
if (type == REDIS_RDB_OPCODE_EXPIRETIME) { if ((expiretime = rdbLoadTime(&rdb)) == -1) goto eoferr; /* We read the time so we need to read the object type again. */ if ((type = rdbLoadType(&rdb)) == -1) goto eoferr; /* the EXPIRETIME opcode specifies time in seconds, so convert * into milliseconds. */ expiretime *= 1000; } else if (type == REDIS_RDB_OPCODE_EXPIRETIME_MS) { /* Milliseconds precision expire times introduced with RDB * version 3. */ if ((expiretime = rdbLoadMillisecondTime(&rdb)) == -1) goto eoferr; /* We read the time so we need to read the object type again. */ if ((type = rdbLoadType(&rdb)) == -1) goto eoferr; }如果OPCode對應的expire指令,需要解析出對應的expire time,然後再次讀取type(對應隨後的kv對中value的類型)。
if (type == REDIS_RDB_OPCODE_EOF) break;
如果OPCode對應的是EOF指令,則RDB載入完成,跳出迴圈。
/* Handle SELECT DB opcode as a special case */ if (type == REDIS_RDB_OPCODE_SELECTDB) { if ((dbid = rdbLoadLen(&rdb,NULL)) == REDIS_RDB_LENERR) goto eoferr; if (dbid >= (unsigned)server.dbnum) { redisLog(REDIS_WARNING,"FATAL: Data file was created with a Redis server configured to handle more than %d databases. Exiting\n", server.dbnum); exit(1); } db = server.db+dbid; continue; }如果OPCode是selectDB指令,讀取DB號,然後切換到對應的DB。
上面把特殊的指令執行完畢,接下來要解析KV對。
/* Read key */ if ((key = rdbLoadStringObject(&rdb)) == NULL) goto eoferr;
讀取字串類型的key。
/* Read value */ if ((val = rdbLoadObject(type,&rdb)) == NULL) goto eoferr;
讀取value,函數rdbLoadObject會根據type的不同,執行不同類型對象的載入,這裡不對該函數展開。
/* Check if the key already expired. This function is used when loading * an RDB file from disk, either at startup, or when an RDB was * received from the master. In the latter case, the master is * responsible for key expiry. If we would expire keys here, the * snapshot taken by the master may not be reflected on the slave. */ if (server.masterhost == NULL && expiretime != -1 && expiretime < now) { decrRefCount(key); decrRefCount(val); continue; }檢測是否到期,如果到期就不會將KV對添加到DB中。
/* Add the new object in the hash table */ dbAdd(db,key,val);
將KV對添加到DB。
/* Set the expire time if needed */ if (expiretime != -1) setExpire(db,key,expiretime); decrRefCount(key);
如果設定了expire time,則添加到expire dict中。
/* Verify the checksum if RDB version is >= 5 */ if (rdbver >= 5 && server.rdb_checksum) { uint64_t cksum, expected = rdb.cksum; if (rioRead(&rdb,&cksum,8) == 0) goto eoferr; memrev64ifbe(&cksum); if (cksum == 0) { redisLog(REDIS_WARNING,"RDB file was saved with checksum disabled: no check performed."); } else if (cksum != expected) { redisLog(REDIS_WARNING,"Wrong RDB checksum. Aborting now."); exit(1); } }檢查check sum。
fclose(fp); stopLoading(); return REDIS_OK;
最後,關閉檔案,並將server.load置為0,表示載入不在進行。
eoferr: /* unexpected end of file is handled here with a fatal exit */ redisLog(REDIS_WARNING,"Short read or OOM loading DB. Unrecoverable error, aborting now."); exit(1); return REDIS_ERR; /* Just to avoid warning */
在上述載入過程中出錯,會跳到eoferr分支。載入出錯時,列印日誌並退出進程。
redis源碼分析(7)——rdb