Redis RDB 持久化詳解

      網友投稿 997 2022-05-28

      Redis 是一種內存數據庫,將數據保存在內存中,讀寫效率要比傳統的將數據保存在磁盤上的數據庫要快很多。但是一旦進程退出,Redis 的數據就會丟失。

      為了解決這個問題,Redis 提供了 RDB 和 AOF 兩種持久化方案,將內存中的數據保存到磁盤中,避免數據丟失。

      antirez 在《Redis 持久化解密》一文中說,一般來說有三種常見的策略來進行持久化操作,防止數據損壞:

      方法1 是數據庫不關心發生故障,在數據文件損壞后通過數據備份或者快照來進行恢復。Redis 的 RDB 持久化就是這種方式。

      方法2 是數據庫使用操作日志,每次操作時記錄操作行為,以便在故障后通過日志恢復到一致性的狀態。因為操作日志是順序追加的方式寫的,所以不會出現操作日志也無法恢復的情況。類似于 Mysql 的 redo 和 undo 日志,具體可以看這篇《InnoDB的磁盤文件及落盤機制》文章。

      方法3 是數據庫不進行老數據的修改,只是以追加方式去完成寫操作,這樣數據本身就是一份日志,這樣就永遠不會出現數據無法恢復的情況了。CouchDB就是此做法的優秀范例。

      RDB 就是第一種方法,它就是把當前 Redis 進程的數據生成時間點快照( point-in-time snapshot ) 保存到存儲設備的過程。

      RDB 的使用

      RDB 觸發機制分為使用指令手動觸發和 redis.conf 配置自動觸發。

      手動觸發 Redis 進行 RDB 持久化的指令的為:

      save ,該指令會阻塞當前 Redis 服務器,執行 save 指令期間,Redis 不能處理其他命令,直到 RDB 過程完成為止。

      bgsave,執行該命令時,Redis 會在后臺異步執行快照操作,此時 Redis 仍然可以相應客戶端請求。具體操作是 Redis 進程執行 fork 操作創建子進程,RDB 持久化過程由子進程負責,完成后自動結束。Redis 只會在 fork 期間發生阻塞,但是一般時間都很短。但是如果 Redis 數據量特別大,fork 時間就會變長,而且占用內存會加倍,這一點需要特別注意。

      自動觸發 RDB 的默認配置如下所示:

      save 900 1 # 表示900 秒內如果至少有 1 個 key 的值變化,則觸發RDB save 300 10 # 表示300 秒內如果至少有 10 個 key 的值變化,則觸發RDB save 60 10000 # 表示60 秒內如果至少有 10000 個 key 的值變化,則觸發RDB

      1

      2

      3

      如果不需要 Redis 進行持久化,那么可以注釋掉所有的 save 行來停用保存功能,也可以直接一個空字符串來停用持久化:save “”。

      Redis 服務器周期操作函數 serverCron 默認每個 100 毫秒就會執行一次,該函數用于正在運行的服務器進行維護,它的一項工作就是檢查 save 選項所設置的條件是否有一項被滿足,如果滿足的話,就執行 bgsave 指令。

      RDB 整體流程

      了解了 RDB 的基礎使用后,我們要繼續深入對 RDB持久化的學習。在此之前,我們可以先思考一下如何實現一個持久化機制,畢竟這是很多中間件所需的一個模塊。

      首先,持久化保存的文件內容結構必須是緊湊的,特別對于數據庫來說,需要持久化的數據量十分大,需要保證持久化文件不至于占用太多存儲。

      Redis RDB 持久化詳解

      其次,進行持久化時,中間件應該還可以快速地響應用戶請求,持久化的操作應該盡量少影響中間件的其他功能。

      最后,畢竟持久化會消耗性能,如何在性能和數據安全性之間做出平衡,如何靈活配置觸發持久化操作。

      接下來我們將帶著這些問題,到源碼中尋求答案。

      本文中的源碼來自 Redis 4.0 ,RDB持久化過程的相關源碼都在 rdb.c 文件中。其中大概的流程如下圖所示。

      上圖表明了三種觸發 RDB 持久化的手段之間的整體關系。通過 serverCron 自動觸發的 RDB 相當于直接調用了 bgsave 指令的流程進行處理。而 bgsave 的處理流程啟動子進程后,調用了 save 指令的處理流程。

      下面我們從 serverCron 自動觸發邏輯開始研究。

      自動觸發 RDB 持久化

      如上圖所示,redisServer 結構體的save_params指向擁有三個值的數組,該數組的值與 redis.conf 文件中 save 配置項一一對應。分別是 save 900 1、save 300 10 和 save 60 10000。dirty 記錄著有多少鍵值發生變化,lastsave記錄著上次 RDB 持久化的時間。

      而 serverCron 函數就是遍歷該數組的值,檢查當前 Redis 狀態是否符合觸發 RDB 持久化的條件,比如說距離上次 RDB 持久化過去了 900 秒并且有至少一條數據發生變更。

      int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { .... /* Check if a background saving or AOF rewrite in progress terminated. */ /* 判斷后臺是否正在進行 rdb 或者 aof 操作 */ if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 || ldbPendingChildren()) { .... } else { // 到這兒就能確定 當前木有進行 rdb 或者 aof 操作 // 遍歷每一個 rdb 保存條件 for (j = 0; j < server.saveparamslen; j++) { struct saveparam *sp = server.saveparams+j; //如果數據保存記錄 大于規定的修改次數 且距離 上一次保存的時間大于規定時間或者上次BGSAVE命令執行成功,才執行 BGSAVE 操作 if (server.dirty >= sp->changes && server.unixtime-server.lastsave > sp->seconds && (server.unixtime-server.lastbgsave_try > CONFIG_BGSAVE_RETRY_DELAY || server.lastbgsave_status == C_OK)) { //記錄日志 serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...", sp->changes, (int)sp->seconds); rdbSaveInfo rsi, *rsiptr; rsiptr = rdbPopulateSaveInfo(&rsi); // 異步保存操作 rdbSaveBackground(server.rdb_filename,rsiptr); break; } } } .... server.cronloops++; return 1000/server.hz; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      35

      36

      如果符合觸發 RDB 持久化的條件,serverCron會調用rdbSaveBackground函數,也就是 bgsave 指令會觸發的函數。

      子進程后臺執行 RDB 持久化

      執行 bgsave 指令時,Redis 會先觸發 bgsaveCommand 進行當前狀態檢查,然后才會調用rdbSaveBackground,其中的邏輯如下圖所示。

      rdbSaveBackground 函數中最主要的工作就是調用 fork 命令生成子流程,然后在子流程中執行 rdbSave函數,也就是 save 指令最終會觸發的函數。

      int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) { pid_t childpid; long long start; // 檢查后臺是否正在執行 aof 或者 rdb 操作 if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR; // 拿出 數據保存記錄,保存為 上次記錄 server.dirty_before_bgsave = server.dirty; // bgsave 時間 server.lastbgsave_try = time(NULL); start = ustime(); // fork 子進程 if ((childpid = fork()) == 0) { int retval; /* 關閉子進程繼承的 socket 監聽 */ closeListeningSockets(0); // 子進程 title 修改 redisSetProcTitle("redis-rdb-bgsave"); // 執行rdb 寫入操作 retval = rdbSave(filename,rsi); // 執行完畢以后 .... // 退出子進程 exitFromChild((retval == C_OK) ? 0 : 1); } else { /* 父進程,進行fork時間的統計和信息記錄,比如說rdb_save_time_start、rdb_child_pid、和rdb_child_type */ .... // rdb 保存開始時間 bgsave 子進程 server.rdb_save_time_start = time(NULL); server.rdb_child_pid = childpid; server.rdb_child_type = RDB_CHILD_TYPE_DISK; updateDictResizePolicy(); return C_OK; } return C_OK; /* unreached */ }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      35

      為什么 Redis 使用子進程而不是線程來進行后臺 RDB 持久化呢?主要是出于Redis性能的考慮,我們知道Redis對客戶端響應請求的工作模型是單進程和單線程的,如果在主進程內啟動一個線程,這樣會造成對數據的競爭條件。所以為了避免使用鎖降低性能,Redis選擇啟動新的子進程,獨立擁有一份父進程的內存拷貝,以此為基礎執行RDB持久化。

      但是需要注意的是,fork 會消耗一定時間,并且父子進程所占據的內存是相同的,當 Redis 鍵值較大時,fork 的時間會很長,這段時間內 Redis 是無法響應其他命令的。除此之外,Redis 占據的內存空間會翻倍。

      生成 RDB 文件,并且持久化到硬盤

      Redis 的 rdbSave 函數是真正進行 RDB 持久化的函數,它的大致流程如下:

      首先打開一個臨時文件,

      調用 rdbSaveRio函數,將當前 Redis 的內存信息寫入到這個臨時文件中,

      接著調用 fflush、fsync 和 fclose 接口將文件寫入磁盤中,

      使用 rename 將臨時文件改名為 正式的 RDB 文件,

      最后記錄 dirty 和 lastsave等狀態信息。這些狀態信息在 serverCron時會使用到。

      int rdbSave(char *filename, rdbSaveInfo *rsi) { char tmpfile[256]; // 當前工作目錄 char cwd[MAXPATHLEN]; FILE *fp; rio rdb; int error = 0; /* 生成tmpfile文件名 temp-[pid].rdb */ snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid()); /* 打開文件 */ fp = fopen(tmpfile,"w"); ..... /* 初始化rio結構 */ rioInitWithFile(&rdb,fp); if (rdbSaveRio(&rdb,&error,RDB_SAVE_NONE,rsi) == C_ERR) { errno = error; goto werr; } if (fflush(fp) == EOF) goto werr; if (fsync(fileno(fp)) == -1) goto werr; if (fclose(fp) == EOF) goto werr; /* 重新命名 rdb 文件,把之前臨時的名稱修改為正式的 rdb 文件名稱 */ if (rename(tmpfile,filename) == -1) { // 異常處理 .... } // 寫入完成,打印日志 serverLog(LL_NOTICE,"DB saved on disk"); // 清理數據保存記錄 server.dirty = 0; // 最后一次完成 SAVE 命令的時間 server.lastsave = time(NULL); // 最后一次 bgsave 的狀態置位 成功 server.lastbgsave_status = C_OK; return C_OK; .... }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      35

      36

      37

      38

      39

      40

      41

      這里要簡單說一下 fflush和fsync的區別。它們倆都是用于刷緩存,但是所屬的層次不同。fflush函數用于 FILE* 指針上,將緩存數據從應用層緩存刷新到內核中,而fsync 函數則更加底層,作用于文件描述符,用于將內核緩存刷新到物理設備上。

      關于 Linux IO 的具體原理可以參考《聊聊Linux IO》

      rdbSaveRio 會將 Redis 內存中的數據以相對緊湊的格式寫入到文件中,其文件格式的示意圖如下所示。

      rdbSaveRio函數的寫入大致流程如下:

      先寫入 REDIS 魔法值,然后是 RDB 文件的版本( rdb_version ),額外輔助信息 ( aux )。輔助信息中包含了 Redis 的版本,內存占用和復制庫( repl-id )和偏移量( repl-offset )等。

      然后 rdbSaveRio 會遍歷當前 Redis 的所有數據庫,將數據庫的信息依次寫入。先寫入 RDB_OPCODE_SELECTDB識別碼和數據庫編號,接著寫入RDB_OPCODE_RESIZEDB識別碼和數據庫鍵值數量和待失效鍵值數量,最后會遍歷所有的鍵值,依次寫入。

      在寫入鍵值時,當該鍵值有失效時間時,會先寫入RDB_OPCODE_EXPIRETIME_MS識別碼和失效時間,然后寫入鍵值類型的識別碼,最后再寫入鍵和值。

      寫完數據庫信息后,還會把 Lua 相關的信息寫入,最后再寫入 RDB_OPCODE_EOF結束符識別碼和校驗值。

      int rdbSaveRio(rio *rdb, int *error, int flags, rdbSaveInfo *rsi) { snprintf(magic,sizeof(magic),"REDIS%04d",RDB_VERSION); /* 1 寫入 magic字符'REDIS' 和 RDB 版本 */ if (rdbWriteRaw(rdb,magic,9) == -1) goto werr; /* 2 寫入輔助信息 REDIS版本,服務器操作系統位數,當前時間,復制信息比如repl-stream-db,repl-id和repl-offset等等數據*/ if (rdbSaveInfoAuxFields(rdb,flags,rsi) == -1) goto werr; /* 3 遍歷每一個數據庫,逐個數據庫數據保存 */ for (j = 0; j < server.dbnum; j++) { /* 獲取數據庫指針地址和數據庫字典 */ redisDb *db = server.db+j; dict *d = db->dict; /* 3.1 寫入數據庫部分的開始標識 */ if (rdbSaveType(rdb,RDB_OPCODE_SELECTDB) == -1) goto werr; /* 3.2 寫入當前數據庫號 */ if (rdbSaveLen(rdb,j) == -1) goto werr; uint32_t db_size, expires_size; /* 獲取數據庫字典大小和過期鍵字典大小 */ db_size = (dictSize(db->dict) <= UINT32_MAX) ? dictSize(db->dict) : UINT32_MAX; expires_size = (dictSize(db->expires) <= UINT32_MAX) ? dictSize(db->expires) : UINT32_MAX; /* 3.3 寫入當前待寫入數據的類型,此處為 RDB_OPCODE_RESIZEDB,表示數據庫大小 */ if (rdbSaveType(rdb,RDB_OPCODE_RESIZEDB) == -1) goto werr; /* 3.4 寫入獲取數據庫字典大小和過期鍵字典大小 */ if (rdbSaveLen(rdb,db_size) == -1) goto werr; if (rdbSaveLen(rdb,expires_size) == -1) goto werr; /* 4 遍歷當前數據庫的鍵值對 */ while((de = dictNext(di)) != NULL) { sds keystr = dictGetKey(de); robj key, *o = dictGetVal(de); long long expire; /* 初始化 key,因為操作的是 key 字符串對象,而不是直接操作 鍵的字符串內容 */ initStaticStringObject(key,keystr); /* 獲取鍵的過期數據 */ expire = getExpire(db,&key); /* 4.1 保存鍵值對數據 */ if (rdbSaveKeyValuePair(rdb,&key,o,expire) == -1) goto werr; } } /* 5 保存 Lua 腳本*/ if (rsi && dictSize(server.lua_scripts)) { di = dictGetIterator(server.lua_scripts); while((de = dictNext(di)) != NULL) { robj *body = dictGetVal(de); if (rdbSaveAuxField(rdb,"lua",3,body->ptr,sdslen(body->ptr)) == -1) goto werr; } dictReleaseIterator(di); } /* 6 寫入結束符 */ if (rdbSaveType(rdb,RDB_OPCODE_EOF) == -1) goto werr; /* 7 寫入CRC64校驗和 */ cksum = rdb->cksum; memrev64ifbe(&cksum); if (rioWrite(rdb,&cksum,8) == 0) goto werr; return C_OK; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      35

      36

      37

      38

      39

      40

      41

      42

      43

      44

      45

      46

      47

      48

      49

      50

      51

      52

      53

      54

      55

      56

      57

      58

      59

      60

      61

      62

      63

      64

      65

      rdbSaveRio在寫鍵值時,會調用rdbSaveKeyValuePair 函數。該函數會依次寫入鍵值的過期時間,鍵的類型,鍵和值。

      int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val, long long expiretime) { /* 如果有過期信息 */ if (expiretime != -1) { /* 保存過期信息標識 */ if (rdbSaveType(rdb,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; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      根據鍵的不同類型寫入不同格式,各種鍵值的類型和格式如下所示。

      Redis 有龐大的對象和數據結構體系,它使用六種底層數據結構構建了包含字符串對象、列表對象、哈希對象、集合對象和有序集合對象的對象系統。感興趣的同學可以參考 《十二張圖帶你了解 Redis 的數據結構和對象系統》一文。

      不同的數據結構進行 RDB 持久化的格式都不同。我們今天只看一下集合對象是如何持久化的。

      ssize_t rdbSaveObject(rio *rdb, robj *o) { ssize_t n = 0, nwritten = 0; .... } else if (o->type == OBJ_SET) { /* Save a set value */ if (o->encoding == OBJ_ENCODING_HT) { dict *set = o->ptr; // 集合迭代器 dictIterator *di = dictGetIterator(set); dictEntry *de; // 寫入集合長度 if ((n = rdbSaveLen(rdb,dictSize(set))) == -1) return -1; nwritten += n; // 遍歷集合元素 while((de = dictNext(di)) != NULL) { sds ele = dictGetKey(de); // 以字符串的形式寫入,因為是SET 所以只寫入 Key 即可 if ((n = rdbSaveRawString(rdb,(unsigned char*)ele,sdslen(ele))) == -1) return -1; nwritten += n; } dictReleaseIterator(di); } ..... return nwritten; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      后記

      歡迎大家留言或者。

      Redis 數據庫

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:昇騰社區modelzoo用例遷移至ModelArts指引
      下一篇:關于Docker中本地倉庫,限制容器資源,容器監控的一些筆記
      相關文章
      亚洲精品免费网站| 亚洲国产片在线观看| 亚洲精品乱码久久久久蜜桃| 国产成+人+综合+亚洲专| 久久久久亚洲AV无码专区首JN| 亚洲AV中文无码字幕色三| 国产精品亚洲а∨无码播放| 亚洲人精品午夜射精日韩| 亚洲精品无码不卡在线播HE| 久久久久久a亚洲欧洲aⅴ| 亚洲宅男天堂在线观看无病毒| 精品亚洲一区二区三区在线播放 | 亚洲乱码中文字幕综合234| 风间由美在线亚洲一区| 黑人粗长大战亚洲女2021国产精品成人免费视频 | 亚洲美日韩Av中文字幕无码久久久妻妇| 天天综合亚洲色在线精品| 国产亚洲人成在线影院| vvvv99日韩精品亚洲| 亚洲精品国产综合久久一线| 久久久久亚洲精品天堂久久久久久 | 亚洲妇女水蜜桃av网网站| 亚洲av日韩av无码av| 亚洲人成网国产最新在线| 亚洲七久久之综合七久久| 亚洲国产无线乱码在线观看 | 亚洲第一中文字幕| 亚洲网站在线观看| 亚洲国产成人久久99精品| 亚洲欧洲无卡二区视頻| 国产成人亚洲精品电影| 国产亚洲精品高清在线| 久久精品国产精品亚洲艾草网| 激情内射亚洲一区二区三区| 亚洲制服在线观看| 亚洲国产AV一区二区三区四区| 怡红院亚洲红怡院在线观看| 久久亚洲精品无码观看不卡| 亚洲va无码va在线va天堂| 亚洲男人电影天堂| 亚洲综合无码一区二区痴汉|