Redis源碼剖析之RDB

      網友投稿 902 2022-05-28

      我們小學三年級的時候就知道,redis是一個純內存存儲的中間件,那它宕機會怎么樣?數據會丟失嗎?答案是可以不丟。 事實上redis為了保證宕機時數據不丟失,提供了兩種數據持久化的機制——rdb和aof。

      rdb就定期將內存里的數據全量dump到磁盤里,下次啟動時就可以直接加載之前的數據了,rdb的問題是它只能提供某個時刻的數據快照,無法保證建立快照后的數據不丟,所以redis還提供了aof。aof全程是Append Only File,它的原理就是把所有改動一條條寫到磁盤上。這篇博客我們來重點介紹下rdb持久性的實現,aof留到下一篇博客。

      rdb相關源碼

      在redis中,觸發rdb保存主要有以下幾種方式。

      save命令

      我們在redis-cli下直接調用save命令就會觸發rdb文件的生成,如果后臺沒有在子進程在生成rdb,就會調用rdbSave()生成rdb文件,并將其保存在磁盤中。

      void saveCommand(client *c) { // 檢查是否后臺已經有進程在執行save,如果有就停止執行。 if (server.child_type == CHILD_TYPE_RDB) { addReplyError(c,"Background save already in progress"); return; } rdbSaveInfo rsi, *rsiptr; rsiptr = rdbPopulateSaveInfo(&rsi); if (rdbSave(server.rdb_filename,rsiptr) == C_OK) { addReply(c,shared.ok); } else { addReplyErrorObject(c,shared.err); } }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

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

      首先創建一個臨時文件。

      創建并初始化rio,rio是redis對io的一種抽象,提供了read、write、flush、checksum……等方法。

      調用 rdbSaveRio(),將當前 Redis 的內存信息全量寫入到臨時文件中。

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

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

      將server.dirty清零,server.dirty是用了記錄在上次生成rdb后有多少次數據變更,會在serverCron中用到。

      具體代碼如下:

      /* rdb磁盤寫入操作 */ int rdbSave(char *filename, rdbSaveInfo *rsi) { char tmpfile[256]; char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */ FILE *fp = NULL; rio rdb; int error = 0; snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid()); fp = fopen(tmpfile,"w"); if (!fp) { char *cwdp = getcwd(cwd,MAXPATHLEN); serverLog(LL_WARNING, "Failed opening the RDB file %s (in server root dir %s) " "for saving: %s", filename, cwdp ? cwdp : "unknown", strerror(errno)); return C_ERR; } rioInitWithFile(&rdb,fp); // 初始化rio, startSaving(RDBFLAGS_NONE); if (server.rdb_save_incremental_fsync) rioSetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES); // 內存數據dump到rdb if (rdbSaveRio(&rdb,&error,RDBFLAGS_NONE,rsi) == C_ERR) { errno = error; goto werr; } /* 把數據刷到磁盤刪,確保操作系統緩沖區沒有剩余數據 */ if (fflush(fp)) goto werr; if (fsync(fileno(fp))) goto werr; if (fclose(fp)) { fp = NULL; goto werr; } fp = NULL; /* 把臨時文件重命名為正式文件名 */ if (rename(tmpfile,filename) == -1) { char *cwdp = getcwd(cwd,MAXPATHLEN); serverLog(LL_WARNING, "Error moving temp DB file %s on the final " "destination %s (in server root dir %s): %s", tmpfile, filename, cwdp ? cwdp : "unknown", strerror(errno)); unlink(tmpfile); stopSaving(0); return C_ERR; } serverLog(LL_NOTICE,"DB saved on disk"); server.dirty = 0; server.lastsave = time(NULL); server.lastbgsave_status = C_OK; stopSaving(1); return C_OK; werr: serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno)); if (fp) fclose(fp); unlink(tmpfile); stopSaving(0); return C_ERR; }

      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

      66

      67

      因為redis是單線程模型,所以在save的過程中處理不了請求,單線程模型可以save的過程中不會有數據變化,但save可能會持續很久,這會導致redis無法正常處理讀寫請求,對于線上服務來說這是非常致命的,所以redis還提供了bgsave命令,它可以在不影響正常讀寫的情況下執行save操作,我們來看下具體實現。

      bgsave命令

      bgsave提供了后臺生成rdb文件的功能,bg含義就是background,具體怎么實現的? 其實就是調用fork() 生成了一個子進程,然后在子進程中完成了save的過程。

      void bgsaveCommand(client *c) { int schedule = 0; /* The SCHEDULE option changes the behavior of BGSAVE when an AOF rewrite * is in progress. Instead of returning an error a BGSAVE gets scheduled. */ if (c->argc > 1) { if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) { schedule = 1; } else { addReplyErrorObject(c,shared.syntaxerr); return; } } rdbSaveInfo rsi, *rsiptr; rsiptr = rdbPopulateSaveInfo(&rsi); if (server.child_type == CHILD_TYPE_RDB) { addReplyError(c,"Background save already in progress"); } else if (hasActiveChildProcess()) { if (schedule) { server.rdb_bgsave_scheduled = 1; // 如果bgsave已經在執行中了,這次執行會放到serverCron中執行 addReplyStatus(c,"Background saving scheduled"); } else { addReplyError(c, "Another child process is active (AOF?): can't BGSAVE right now. " "Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever " "possible."); } } else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) { addReplyStatus(c,"Background saving started"); } else { addReplyErrorObject(c,shared.err); } } int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) { pid_t childpid; if (hasActiveChildProcess()) return C_ERR; server.dirty_before_bgsave = server.dirty; server.lastbgsave_try = time(NULL); // 創建子進程,redisFork實際就是對fork的封裝 if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) { int retval; /* 子進程 */ redisSetProcTitle("redis-rdb-bgsave"); redisSetCpuAffinity(server.bgsave_cpulist); retval = rdbSave(filename,rsi); if (retval == C_OK) { sendChildCowInfo(CHILD_INFO_TYPE_RDB_COW_SIZE, "RDB"); } exitFromChild((retval == C_OK) ? 0 : 1); } else { /* 父進程 */ if (childpid == -1) { server.lastbgsave_status = C_ERR; serverLog(LL_WARNING,"Can't save in background: fork: %s", strerror(errno)); return C_ERR; } serverLog(LL_NOTICE,"Background saving started by pid %ld",(long) childpid); server.rdb_save_time_start = time(NULL); server.rdb_child_type = RDB_CHILD_TYPE_DISK; 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

      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

      66

      67

      68

      69

      70

      bgsave其實就是把save的流程放到子進程里執行,這樣就不會阻塞到父進程了。我最開始看到這里的時候有個問題,父進程在持續讀寫內存的情況下子進程是如何保存某一時刻快照的? 這個redid中沒有特殊處理,還是依賴了操作系統提供的fork()。

      當一個進程調用fork()時,操作系統會復制一份當前的進程,包括當前進程中的內存內容。所以可以認為只要fork()成功,當前內存中的數據就被全量復制了一份。當然具體實現上內核為了提升fork()的性能,使用了copy-on-write的技術,只有被復制的數據在被父進程或者子進程改動時才會真正拷貝。

      serverCron

      上面生成rdb的兩種方式都是被動觸發的,redis也提供定期生成rdb的機制。redis關于rdb生成的配置如下:

      save ## 例如 save 3600 1 # 3600秒內如果有1條寫書就生成rdb save 300 100 # 300秒內如果有100條寫書就生成rdb save 60 10000 # 60秒內如果有1000條寫書就生成rdb

      1

      2

      3

      4

      5

      定期生成rdb的實現在server.c 中的serverCron中。serverCron是redis每次執行完一次eventloop執行的定期調度任務,里面就有rdb和aof的執行邏輯,rdb相關具體如下:

      int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { /* . 略去其他代碼 */ /* 檢測bgsave和aof重寫是否在執行過程中 */ if (hasActiveChildProcess() || ldbPendingChildren()) { run_with_period(1000) receiveChildInfo(); checkChildrenDone(); } else { /* If there is not a background saving/rewrite in progress check if * we have to save/rewrite now. */ for (j = 0; j < server.saveparamslen; j++) { struct saveparam *sp = server.saveparams+j; /* 檢查是否達到了執行save的標準 */ 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; } } } /* . 略去其他代碼 */ /* 如果上次觸發bgsave時已經有進程在執行了,就會標記rdb_bgsave_scheduled=1,然后放到serverCron * 中執行 */ if (!hasActiveChildProcess() && server.rdb_bgsave_scheduled && (server.unixtime-server.lastbgsave_try > CONFIG_BGSAVE_RETRY_DELAY || server.lastbgsave_status == C_OK)) { rdbSaveInfo rsi, *rsiptr; rsiptr = rdbPopulateSaveInfo(&rsi); if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) server.rdb_bgsave_scheduled = 0; } /* . 略去其他代碼 */ }

      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

      Redis源碼剖析之RDB

      45

      46

      47

      48

      49

      50

      51

      rdb文件格式

      rdb的具體文件格式相對比較簡單,具體如下:

      ----------------------------# 52 45 44 49 53 # 魔術 "REDIS" 30 30 30 33 # ASCII碼rdb的版本號 "0003" = 3 ---------------------------- FA # 輔助字段 $string-encoded-key # 可能包含多個元信息 $string-encoded-value # 比如redis版本號,創建時間,內存使用量……... ---------------------------- FE 00 # redis db號. db number = 00 FB # 標識db的大小 $length-encoded-int # hash表的大小(int) $length-encoded-int # expire hash表的大小(int) ----------------------------# 從這里開始就是具體的k-v數據 FD $unsigned-int # 數據還有多少秒過期(4byte unsigned int) $value-type # 標識value數據類型(1 byte) $string-encoded-key # key,redis字符串類型(sds) $encoded-value # value, 類型取決于 $value-type ---------------------------- FC $unsigned long # 數據還有多少毫秒過期(8byte unsigned long) $value-type # 標識value數據類型(1 byte) $string-encoded-key # key,redis字符串類型(sds) $encoded-value # value, 類型取決于 $value-type ---------------------------- $value-type # redis數據key-value,沒有過期時間 $string-encoded-key $encoded-value ---------------------------- FE $length-encoding # FE標識前一個db的數據結束,然后再加上數據的長度 ---------------------------- ... # 其他redis db中的k-v數據, ... FF # FF rdb文件的結束標識 8-byte-checksum ## 最后是8byte的CRC64校驗和

      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

      總結

      rdb在一定程度上保證了redis實例在異常宕機時數據不丟,當因為是定期生成的rdb快照,在生成快照后產生的變動無法追加到rdb文件中,所以rdb無法徹底保證數據不丟,為此redis又提供了另外一種數據持久化機制aof,我們將在下篇文章中看到。另外,在執行bgsave的時候高度依賴于操作系統的fork()機制,這也是會帶來很大的性能開銷的,詳見Linux fork隱藏的開銷-過時的fork(正傳)

      參考資料

      Redis RDB 持久化詳解

      https://rdb.fnordig.de/file_format.html

      Redis Persistence

      Linux fork隱藏的開銷-過時的fork(正傳)

      本文是Redis源碼剖析系列博文,同時也有與之對應的Redis中文注釋版,有想深入學習Redis的同學,歡迎star和關注。

      Redis中文注解版倉庫:https://github.com/xindoo/Redis

      Redis源碼剖析專欄:https://zxs.io/s/1h

      如果覺得本文對你有用,歡迎一鍵三連。

      Redis 任務調度

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

      上一篇:240_Redis_數據持久化RDB_AOF
      下一篇:如何在鯤鵬服務器快速部署docker+docker-compose環境
      相關文章
      亚洲人成色77777| 伊人久久综在合线亚洲91 | 亚洲日本va在线视频观看| 亚洲AV成人片无码网站| 亚洲三级高清免费| 亚洲日本香蕉视频观看视频| 亚洲视频一区网站| 亚洲日本国产乱码va在线观看| 亚洲精品国产情侣av在线| 亚洲熟妇av一区二区三区下载 | 午夜亚洲国产成人不卡在线| 亚洲av纯肉无码精品动漫| 国产亚洲欧美在线观看| 极品色天使在线婷婷天堂亚洲| 日日摸日日碰夜夜爽亚洲| 最新亚洲人成无码网站| 亚洲成a人无码av波多野按摩| 亚洲国产精品自产在线播放| 2048亚洲精品国产| 亚洲色欲色欲www在线丝| 久久精品亚洲中文字幕无码网站| 亚洲国产精品自在在线观看| 久久狠狠高潮亚洲精品| 亚洲另类图片另类电影| 亚洲中文字幕无码久久2020| 亚洲sm另类一区二区三区| 日韩亚洲人成网站| 久久精品国产亚洲精品| 国产精品亚洲片在线| 亚洲伦另类中文字幕| 亚洲国产日韩在线| 亚洲综合av一区二区三区不卡 | 风间由美在线亚洲一区| 亚洲第一黄色网址| 亚洲一区二区三区香蕉| 中文字幕在线观看亚洲| 亚洲制服丝袜中文字幕| 337p日本欧洲亚洲大胆人人| 精品国产日韩亚洲一区| 久久久久久亚洲av成人无码国产| 亚洲日本香蕉视频|