在啟動並執行情況下,redis將資料以資料結構的形式儲存在記憶體中,為了讓資料在redis重啟之後仍然可用,Redis提供了RDB和AOF兩種持久化方式,本篇講解RDB持久化的工作方式
在運行時,redis將記憶體中的資料庫以快照的方式儲存到磁碟中,在重啟後讀取磁碟中RDB檔案來將redis恢複到原來的狀態 RDB的優缺點 優點
·RDB是一個非常緊湊的檔案,它儲存了redis在某個時間點上的資料集,非常適於備份
·RDB適用於災難恢複,因為其只有一個檔案,而且內容緊湊,可以將其傳送到其他的資料中心用於儲存
·RDB可以最大化redis的效能,執行RDB持久化時只需要fork一個子進程,並由子進程進行持久化工作,父進程不需要處理任何磁碟I/O操作
·RDB在恢複大資料集時比AOF要快 缺點
·如果你盡量避免伺服器故障時遺失資料,那麼RDB並不適合。因為RDB持久化要儲存整個資料集的資料,這不是一個輕鬆的操作,所以不適合頻繁操作,一旦伺服器發生故障有可能會丟失幾分鐘的資料
·每次儲存RDB時,父進程都需要fork一個子進程進行持久化的操作,如果資料集非常的龐大,fork所需的時間可能非常長,這樣可能造成暫停處理用戶端的請求
如果資料集非常巨大,並且 CPU 時間非常緊張的話,那麼這種停止時間甚至可能會長達整整一秒。雖然 AOF 重寫也需要進行 fork() ,但無論 AOF 重寫的執行間隔有多長,資料的耐久性都不會有任何損失 RDB快照
在預設情況下,Redis 將資料庫快照集儲存在名字為 dump.rdb 的二進位檔案中。你可以對 Redis 進行設定,讓它在“N 秒內資料集至少有 M 個改動”這一條件被滿足時,自動儲存一次資料集。你也可以通過調用SAVE 或者BGSAVE ,手動讓 Redis 進行資料集儲存操作
比如說,以下設定會讓 Redis 在滿足“60 秒內有至少有 1000 個鍵被改動”這一條件時,自動儲存一次資料集:
save 60 1000
這種持久化方式被稱為快照(snapshot) 格式
一個RDB檔案可以分為下面幾個部分:
REDIS: 檔案開頭儲存著 REDIS 5個字元,標示這一個RDB檔案的開始
RDB-VERSION:一個四位元組長的以字元表示的整數,記錄了該檔案所使用的 RDB 版本號碼
DB-DATA:這一部分會在一個RDB檔案中出現不止一次,這個部分儲存了redis中非空資料庫的所有資料
SELECT-DB:這個欄位儲存著後面索引值對所屬的資料庫序號
KEY-VALUE-PAIRS:因為空白的資料庫不會被儲存到 RDB 檔案,所以這個部分至少會包含一個索引值對的資料。
每個索引值對的資料使用以下結構來儲存:
OPTIONAL-EXPIRE-TIME 域是可選的,如果鍵沒有設定到期時間,那麼這個域就不會出現;反之,如果這個域出現的話,那麼它記錄著鍵的到期時間,在目前的版本的 RDB 中,到期時間是一個以毫秒為單位的 UNIX 時間戳記
KEY 域儲存著鍵,格式和 REDIS_ENCODING_RAW 編碼的字串對象一樣
TYPE-OF-VALUE 域記錄著 VALUE 域的值所使用的編碼,根據這個域的指示,程式會使用不同的方式來儲存和讀取 VALUE 的值
EOF:標示資料庫內容的結尾,EDIS_RDB_OPCODE_EOF
CHECK-SUM:RDB 檔案所有內容的校正和,一個 uint_64t 類型值
REDIS 在寫入 RDB 檔案時將校正和儲存在 RDB 檔案的末尾,當讀取時,根據它的值對內容進行校正 RDB持久化命令 SAVE
當SAVE命令執行時,Redis伺服器是阻塞的,所以當一個SAVE命令執行時,不會處理另外的SAVE、BGSAVE或者BGREWRITEAOF命令
只有在上一個 SAVE 執行完畢、Redis 重新開始接受請求之後,新的 SAVE 、BGSAVE 或BGREWRITEAOF 命令才會被處理
另外,因為 AOF 寫入由後台線程完成,而 BGREWRITEAOF 則由子進程完成,所以在 SAVE執行的過程中,AOF 寫入和 BGREWRITEAOF 可以同時進行 BGSAVE
在執行 SAVE 命令之前,伺服器會檢查 BGSAVE 是否正在執行當中,如果是的話,伺服器就不調用 rdbSave ,而是向用戶端返回一個出錯資訊,告知在 BGSAVE 執行期間,不能執行SAVE
這樣做可以避免 SAVE 和 BGSAVE 調用的兩個 rdbSave 交叉執行,造成競爭條件
另一方面,當 BGSAVE 正在執行時,調用新 BGSAVE 命令的用戶端會收到一個出錯資訊,告
知 BGSAVE 已經在執行當中
BGREWRITEAOF 和 BGSAVE 不能同時執行:
• 如果 BGSAVE 正在執行,那麼 BGREWRITEAOF 的重寫請求會被延遲到 BGSAVE 執行完畢之後進行,執行 BGREWRITEAOF 命令的用戶端會收到請求被延遲的回複。
• 如果 BGREWRITEAOF 正在執行,那麼調用 BGSAVE 的用戶端將收到出錯資訊,表示這兩個命令不能同時執行。
BGREWRITEAOF 和 BGSAVE 兩個命令在操作方面並沒有什麼衝突的地方,不能同時執行它們只是一個效能方面的考慮:並發出兩個子進程,並且兩個子進程都同時進行大量的磁碟寫入操作,這怎麼想都不會是一個好主意 RDB的實現
RDB 功能最核心的是 rdbSave 和 rdbLoad 兩個函數
下面具體分析rdbSave函數的實現
redis 支援兩種方式進行 RDB:
當前進程執行和後台執行(BGSAVE)
RDB BGSAVE 策略是 fork 出一個子進程,把記憶體中的資料集整個 dump 到硬碟上。兩個情境舉例 redis 伺服器初始化過程中,設定了定時事件,每隔一段時間就會觸發持久化操作;進入定時事件處理常式中,就會 fork 產生子進程執行持久化操作 redis 伺服器預設了 save 指令,用戶端可要求伺服器處理序中斷服務,執行持久化操作
rdbSave函數實現如下:
1: /* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success */
2: /*
3: * 將資料庫儲存到磁碟上。成功返回 REDIS_OK ,失敗返回 REDIS_ERR 。
4: */
5: int rdbSave(char *filename) {
6: dictIterator *di = NULL;
7: dictEntry *de;
8: char tmpfile[256];
9: char magic[10];
10: int j;
11: long long now = mstime();
12: FILE *fp;
13: rio rdb;
14: uint64_t cksum;
15:
16: // 以 "temp-<pid>.rdb" 格式建立臨時檔案名稱
17: snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
18: fp = fopen(tmpfile,"w");
19: if (!fp) {
20: redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s",
21: strerror(errno));
22: return REDIS_ERR;
23: }
24:
25: // 初始化 rio 檔案
26: rioInitWithFile(&rdb,fp);
27: // 如果有需要的話,設定校正和計算函數
28: if (server.rdb_checksum)
29: rdb.update_cksum = rioGenericUpdateChecksum;
30: // 以 "REDIS <VERSION>" 格式寫入檔案頭,以及 RDB 的版本
31: snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);
32: if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;
33:
34: // 遍曆所有資料庫,儲存它們的資料
35: for (j = 0; j < server.dbnum; j++) {
36: // 指向資料庫
37: redisDb *db = server.db+j;
38: // 指向資料庫 key space
39: dict *d = db->dict;
40: // 資料庫為空白, pass ,處理下個資料庫
41: if (dictSize(d) == 0) continue;
42:
43: // 建立迭代器
44: di = dictGetSafeIterator(d);
45: if (!di) {
46: fclose(fp);
47: return REDIS_ERR;
48: }
49:
50: /* Write the SELECT DB opcode */
51: // 記錄正在使用的資料庫的號碼
52: if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;
53: if (rdbSaveLen(&rdb,j) == -1) goto werr;
54:
55: /* Iterate this DB writing every entry */
56: // 將資料庫中的所有節點儲存到 RDB 檔案
57: while((de = dictNext(di)) != NULL) {
58: // 取出鍵
59: sds keystr = dictGetKey(de);
60: // 取出值
61: robj key,
62: *o = dictGetVal(de);
63: long long expire;
64:
65: initStaticStringObject(key,keystr);
66: // 取出到期時間
67: expire = getExpire(db,&key);
68: if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr;
69: }
70: dictReleaseIterator(di);
71: }
72: di = NULL; /* So that we don't release it again on error. */
73:
74: /* EOF opcode */
75: if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;
76:
77: /* CRC64 checksum. It will be zero if checksum computation is disabled, the
78: * loading code skips the check in this case. */
79: cksum = rdb.cksum;
80: memrev64ifbe(&cksum);
81: rioWrite(&rdb,&cksum,8);
82:
83: /* Make sure data will not remain on the OS's output buffers */
84: fflush(fp);
85: fsync(fileno(fp));
86: fclose(fp);
87:
88: /* Use RENAME to make sure the DB file is changed atomically only
89: * if the generate DB file is ok. */
90: // 將臨時檔案 tmpfile 改名為 filename
91: if (rename(tmpfile,filename) == -1) {
92: redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
93: unlink(tmpfile);
94: return REDIS_ERR;
95: }
96:
97: redisLog(REDIS_NOTICE,"DB saved on disk");
98:
99: // 初始化資料庫資料
100: server.dirty = 0;
101: server.lastsave = time(NULL);
102: server.lastbgsave_status = REDIS_OK;
103:
104: return REDIS_OK;
105:
106: werr:
107: fclose(fp);
108: unlink(tmpfile);
109: redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
110: if (di) dictReleaseIterator(di);
111: return REDIS_ERR;
112: }
在rdbSave()中,首先建立一個臨時檔案,在成功save後會改名,然後初始化rio(robust IO,是對IO操作的封裝,在Redis中專門用於ddb save),然後在檔案中首先記錄REDIS MAGIC NUMBER。這裡的rdb_checksum是校正和,每當向硬碟寫時,都會更新校正和,用於後面讀取rdb檔案時校正
dictGetSafeIterator()是得到dict資料結構的一個迭代器,用於遍曆Redis Instance中的資料庫,首先寫入REDIS_RDB_OPCODE_SELECTDB首碼和DB編號。這裡的rdbSaveLen()就是實現長度壓縮編碼的函數,用6bits、14bits或者32bits儲存長度。然後遍曆DB,獲得key和value,還有可能具有的expire time。因為在DB層,key是ads結構的字串,需要先封裝為robj對象,再根據String類型的儲存方式儲存。調用rdbSaveKeyValuePair()解析value類型,根據類型儲存格式
最後寫入校正和,同步內容到硬碟,然後重新命名檔案
以下為bgsave,fork產生一個子進程
1:
2: /*
3: * 使用子進程儲存資料庫資料,不阻塞主進程
4: */
5: int rdbSaveBackground(char *filename) {
6: pid_t childpid;
7: long long start;
8:
9: if (server.rdb_child_pid != -1) return REDIS_ERR;
10:
11: // 修改伺服器狀態
12: server.dirty_before_bgsave = server.dirty;
13:
14: // 開始時間
15: start = ustime();
16: // 建立子進程
17: if ((childpid = fork()) == 0) {
18: int retval;
19:
20: /* Child */
21: // 子進程不接收網路資料
22: if (server.ipfd > 0) close(server.ipfd);
23: if (server.sofd > 0) close(server.sofd);
24:
25: // 儲存資料
26: retval = rdbSave(filename);
27: if (retval == REDIS_OK) {
28: size_t private_dirty = zmalloc_get_private_dirty();
29:
30: if (private_dirty) {
31: redisLog(REDIS_NOTICE,
32: "RDB: %lu MB of memory used by copy-on-write",
<