聊聊Redis持久化策略RDB

寫在文章開頭

為避免服務器宕機著情況導致redis內存數據庫數據丟失,redis默認出通過rdb保證可靠性,本文將從源碼的角度帶讀者了解rdb讀寫時機和寫入流程。

在這里插入圖片描述

Hi,我是 sharkChili ,是個不斷在硬核技術上作死的 java coder ,是 CSDN的博客專家 ,也是開源項目 Java Guide 的維護者之一,熟悉 Java 也會一點 Go ,偶爾也會在 C源碼 邊緣徘徊。寫過很多有意思的技術博客,也還在研究并輸出技術的路上,希望我的文章對你有幫助,非常歡迎你關注我的公眾號: 寫代碼的SharkChili

因為近期收到很多讀者的私信,所以也專門創建了一個交流群,感興趣的讀者可以通過上方的公眾號獲取筆者的聯系方式完成好友添加,點擊備注 “加群” 即可和筆者和筆者的朋友們進行深入交流。

在這里插入圖片描述

詳解RDB持久化

save指令觸發rdb

redis支持通過命令的方式持久化內存數據庫數據,當我們鍵入save的時候,redis解析到這個指令之后,主線程直接調用saveCommand方法生成rdb文件落到磁盤中。

在這里插入圖片描述

我們可以在rdb.c文件中看到該方法的實現,可以看到為了避免臟寫等問題,saveCommand會檢查當前是否有rdb子進程執行,如果沒有在子進程執行rdb持久化則直接調用rdbSave方法生成dump.rdb文件落盤:

//調用save指令其內部調用rdbSave完成rdb文件生成
void saveCommand(redisClient *c) {//檢查是否子進程執行rdb,若有則直接返回if (server.rdb_child_pid != -1) {addReplyError(c,"Background save already in progress");return;}//調用rdbSaveif (rdbSave(server.rdb_filename) == REDIS_OK) {addReply(c,shared.ok);} else {addReply(c,shared.err);}
}

步入rdbSave即可看到生成臨時rdb寫入數據,然后數據刷盤,最后完成文件名原子修改的操作:

int rdbSave(char *filename) {char tmpfile[256];FILE *fp;rio rdb;int error;//生成一個tmp文件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;}//調用rdbSaveRio完成數據寫入rioInitWithFile(&rdb,fp);if (rdbSaveRio(&rdb,&error) == REDIS_ERR) {errno = error;goto werr;}//直接刷盤到磁盤,避免留在系統輸出緩沖區/* 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;//完成寫入后文件重命名為dump.rdbif (rename(tmpfile,filename) == -1) {redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));unlink(tmpfile);return REDIS_ERR;}//......return REDIS_OK;//......
}

bgsave指令觸發rdb

同時redis也支持后臺持久化,如果用戶需要考慮redis性能問題,可以直接通過bgsave指令創建rdb子進程完成數據庫數據持久化。

在這里插入圖片描述

我們同樣可以在rdb.c文件中看到bgsave指令調用的方法bgsaveCommand,可以看到如果沒有子進程進行rdb或者aof,該指令會調用rdbSaveBackground完成異步數據持久化:

//調用rdbSaveBackground創建一個子進程生成rdb文件,不影響主線程
void bgsaveCommand(redisClient *c) {//如果有子進程執行rdb或者aof,則直接返回錯誤提醒if (server.rdb_child_pid != -1) {addReplyError(c,"Background save already in progress");} else if (server.aof_child_pid != -1) {addReplyError(c,"Can't BGSAVE while AOF log rewriting is in progress");} else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {//調用rdbSaveBackground進行數據持久化addReplyStatus(c,"Background saving started");} else {addReply(c,shared.err);}
}

步入rdbSaveBackground可以看到,其內部還會檢查一次是否有文件進行rdb,如果明確沒有之后直接fork一個子進程出來調用上文所說的rdbSave完成數據持久化到dump.rdb中:

int rdbSaveBackground(char *filename) {pid_t childpid;long long start;if (server.rdb_child_pid != -1) return REDIS_ERR;//......start = ustime();if ((childpid = fork()) == 0) {//創建子進程int retval;/* Child */closeListeningSockets(0);redisSetProcTitle("redis-rdb-bgsave");retval = rdbSave(filename);//生成rdb文件if (retval == REDIS_OK) {//......}exitFromChild((retval == REDIS_OK) ? 0 : 1);//退出子進程} else {//......}return REDIS_OK; /* unreached */
}

RDB被動觸發

redis被動觸發由時間事件輪詢處理,我們可以在redis.conf配置rdb被動觸發持久化的時機,默認配置如下當60s生成10000或者300 生成10次改變亦或者900s生成1s,我們就會執行一次被動rdb持久化:

save 900 1
save 300 10
save 60 10000

在這里插入圖片描述

對應的我們可以在redis.cserverCron函數在看到這段邏輯,它會遍歷出我們配置的保存間隔配置saveparam,通過比對這3條配置的上次保存時間計算出時間間隔,以及當前redis變化書dirty看看是否符合要求,若如何要求則進行后臺rdb持久化:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {//....../* Check if a background saving or AOF rewrite in progress terminated. */if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {//......}} else {//遍歷3個配置的params,如果改變數和事件間隔配置要求則直接進行后臺被動rdb持久化for (j = 0; j < server.saveparamslen; j++) {struct saveparam *sp = server.saveparams+j;if (server.dirty >= sp->changes && //查看變化數是否大于當前配置的changesserver.unixtime-server.lastsave > sp->seconds && //查看時間間隔是否大于配置(server.unixtime-server.lastbgsave_try >REDIS_BGSAVE_RETRY_DELAY ||server.lastbgsave_status == REDIS_OK)){//......//執行異步持久化rdbSaveBackground(server.rdb_filename);break;}}//......}}//......return 1000/server.hz;
}

其他被動落盤時機

其實有些時候我們執行的某些執行也會進行rdb持久化,例如flushall刷盤指令,其調用函數flushallCommand就會時間串行執行rdb持久化:

//調用flush指令時會調用rdbSave進行數據持久化
void flushallCommand(redisClient *c) {//......if (server.saveparamslen > 0) {//串行執行rdb持久化int saved_dirty = server.dirty;rdbSave(server.rdb_filename);//......}server.dirty++;
}

當我們關閉redis服務器的時候也會執行rdb串行持久化:

//服務器進程關閉時調用rdbSave生成rdb文件
int prepareForShutdown(int flags) {//......if (server.rdb_child_pid != -1) {//......}if (server.aof_state != REDIS_AOF_OFF) {//......}if ((server.saveparamslen > 0 && !nosave) || save) {if (rdbSave(server.rdb_filename) != REDIS_OK) {//......return REDIS_ERR;}}//......return REDIS_OK;
}

rdb寫入文件數據詳解

無論是rdbsave還是rdbbgsave對應的方法,其內部都會調用rdbSaveRio,它進行文件寫入時對應寫入數據大體順序是:

  1. 寫入REDIS大寫。
  2. 補0填充長度。
  3. 寫入當前redis版本號,以筆者源碼為例則是6。
  4. 遍歷數據庫寫入REDIS_RDB_OPCODE_SELECTDB表示開始存儲數據庫數據,這個值默認為254,redis會轉為八進制376寫入。
  5. 遍歷當前數據庫鍵值對key長度和keyvalue長度和value寫入,后續數據庫都是如此往復。
  6. 所有數據庫寫完后補REDIS_RDB_OPCODE_EOF和checksum用于后續rdb數據恢復的校驗。

為保證讀者更直觀的了解redis持久化寫入的內容,我們可以刪除本地rdb文件,然后執行如下執行生成一個全新的rdb文件:

# 保存鍵值對
set key value
# 切換到1庫
select 1
# 保存鍵值對到1庫
set key-1 value
# 調用save進行數據持久化
save

正常情況下我們打開rdb文件會得到一堆類型亂碼的內容,我們無法知曉寫入的信息,我們可以直接鍵入od生成rdb文件16進制數據及其對應的ASCII字符:

od -A x -t x1c -v dump.rdb

最終我們就可以得到如下文件,可以看到數據格式和筆者上文所說基本一致:

#        大寫REDIS          補0            254的8進制 當前數據庫索引   鍵值對`key`長度和`key`,`value`長度和`value`      
#000000  52  45  44  49  53  30  30  30  36  fe  00  00  03  6b  65  79R   E   D   I   S   0   0   0   6 376  \0  \0 003   k   e   y
000010  05  76  61  6c  75  65  fe  01  00  05  6b  65  79  2d  31  05005   v   a   l   u   e 
#  254的8進制 當前數據庫索引1  鍵值對key長度和key,value長度和value    
376 001  \0 005   k   e   y   -   1 005
000020  76  61  6c  75  65  ff  76  eb  e4  80  bd  df  66  11v   a   l   u   e 
# EOF 255八進制 剩下8位是對應的checksum
377   v 353 344 200 275 337   f 021
00002e

對應的我們給出這段源碼,對應的寫入流程如上文筆者所述:

int rdbSaveRio(rio *rdb, int *error) {dictIterator *di = NULL;dictEntry *de;char magic[10];int j;long long now = mstime();uint64_t cksum;if (server.rdb_checksum)rdb->update_cksum = rioGenericUpdateChecksum;snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);//對應redis 3個0 然后版本號,當前版本為6if (rdbWriteRaw(rdb,magic,9) == -1) goto werr;//上述魔數寫入rdb文件//遍歷數據庫for (j = 0; j < server.dbnum; j++) {redisDb *db = server.db+j;dict *d = db->dict;if (dictSize(d) == 0) continue;di = dictGetSafeIterator(d);if (!di) return REDIS_ERR;/* Write the SELECT DB opcode */if (rdbSaveType(rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;//寫入254,也就是內容中的376if (rdbSaveLen(rdb,j) == -1) goto werr;//寫入當前庫索引//遍歷當前鍵值對寫入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);}//....../* EOF opcode */if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;//寫入結束符254 八進制為377cksum = rdb->cksum;memrev64ifbe(&cksum);if (rioWrite(rdb,&cksum,8) == 0) goto werr;//寫入8位數校驗和,其底層調用rioGenericUpdateChecksum,按照cksum到數組中獲取就對應的值并return REDIS_OK;//......
}

對應的我們步入rdbSaveKeyValuePair即可看到redis獲取key長度和key,以及value長度和value并寫入rdb文件的核心流程:

int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,long long expiretime, long long now)
{//....../* Save type, key, value */if (rdbSaveObjectType(rdb,val) == -1) return -1;//寫入類型以字符串形式就是0if (rdbSaveStringObject(rdb,key) == -1) return -1;//寫入key長度和keyif (rdbSaveObject(rdb,val) == -1) return -1;//寫入value長度和valuereturn 1;
}

小結

自此我們將redis持久化策略rdb都分析完成了,希望對你有幫助。

我是 sharkchiliCSDN Java 領域博客專家開源項目—JavaGuide contributor,我想寫一些有意思的東西,希望對你有幫助,如果你想實時收到我寫的硬核的文章也歡迎你關注我的公眾號: 寫代碼的SharkChili
因為近期收到很多讀者的私信,所以也專門創建了一個交流群,感興趣的讀者可以通過上方的公眾號獲取筆者的聯系方式完成好友添加,點擊備注 “加群” 即可和筆者和筆者的朋友們進行深入交流。

在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/39798.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/39798.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/39798.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

刷代碼隨想錄有感(124):動態規劃——最長公共子序列

題干&#xff1a; 代碼&#xff1a; class Solution { public:int findLength(vector<int>& nums1, vector<int>& nums2) {vector<vector<int>>dp(nums1.size() 1, vector<int>(nums2.size() 1, 0));int res 0;for(int i 1; i <…

數據集采樣策略對模型性能的影響問題

數據集采樣策略對模型性能的影響問題&#xff0c;需要具體代碼示例 隨著機器學習和深度學習的快速發展&#xff0c;數據集的質量和規模對于模型性能的影響變得越來越重要。在實際應用中&#xff0c;我們往往面臨著數據集規模過大、樣本類別不平衡、樣本噪聲等問題。這時&#…

uni.showShareMenu({}) 和 uni.showShareImageMenu({}) 的區別

ChatGPT uni.showShareMenu({}) 和 uni.showShareImageMenu({}) 是 Uni-app 中兩個不同的 API&#xff0c;它們的作用和用法有所不同&#xff1a; uni.showShareMenu({}) 作用&#xff1a;用于顯示當前頁面的分享菜單&#xff0c;通常顯示在頁面的右上角&#xff08;類似于微…

lnternet 發展史

一&#xff0c;lnternet 發展史 ARPA net &#xff08;上世紀50年代二戰結束&#xff09; 無線 戰場指揮通信協議落后 TCP/IP 包交換 WEB (70年代 ) 80年代 90年代 二&#xff0c;互聯網的典型應用&#xff1a; 96年到2008年 第一代技術…

AJAX的概述 ,同步和異步的區別 ,AJAX 的交互模型和傳統交互模型的區別

一. AJAX的概述 1.1 什么是ajax 同步&#xff1a; 異步&#xff1a; 1.AJAX Asynchronous JavaScript and XML&#xff08;異步的 JavaScript 和 XML&#xff09;。 ? 說明&#xff1a;異步&#xff1a;就是不同步。例如我們向后臺發送請求&#xff0c;同步的方式是后臺必…

日語筆記——jy

敬語尊敬自兼丁寧するされるいたすします先生が詳細に説明されるご説明いたします説明しますいうおっしゃる申す言うお名まえはなんとおっしゃいますかほかのことは申すまでもない親の言う事をよく聞く行くいらっしゃる參る行きます先生もいらっしゃるのですかご一緒に參りまし…

Node.js學習路線

Node.js是一個基于Chrome V8引擎的JavaScript運行環境&#xff0c;它允許JavaScript在服務器端運行。Node.js的核心內容和高階內容涵蓋了多個方面&#xff0c;以下是對Node.js的詳細解析、核心內容以及高階內容的歸納&#xff1a; 一、Node.js簡介 運行環境&#xff1a;Node.…

svn忽略上傳文件node_modules文件

文章目錄 1.點擊svn項目右鍵-》選中svn的屬性2. 點擊 新建3. 點擊其他4. 選擇屬性 svn:global-ignores5. 輸入忽略文件 1.點擊svn項目右鍵-》選中svn的屬性 2. 點擊 新建 3. 點擊其他 4. 選擇屬性 svn:global-ignores 5. 輸入忽略文件

四、【源碼】Bean屬性注入

源碼地址&#xff1a;https://github.com/spring-projects/spring-framework 倉庫地址&#xff1a;https://gitcode.net/qq_42665745/spring/-/tree/04-porperty-inject Bean屬性注入 屬性注入相關的類 1.PropertyValue&#xff1a;屬性對象&#xff0c;name:value 2.Prope…

【Asterinas】Asterinas 進程啟動與切換

Asterinas 進程啟動與切換 進程啟動 進程創建&#xff1a; Rust pub fn spawn_user_process( executable_path: &str, argv: Vec, envp: Vec, ) -> Result<Arc> { // spawn user process should give an absolute path debug_assert!(executable_path.starts_with…

數據結構 —— 二叉樹

1.樹的概念及結構 1.1樹的概念 樹是一種非線性的數據結構&#xff0c;它有著多分支&#xff0c;層次性的特點。 由于其形態類似于自然界中倒過來的數&#xff0c;所以我們將這種數據結構稱為“樹形結構” 注意&#xff1a; 樹形結構中&#xff0c;子樹之間不能有交集&#x…

降重工具大揭秘:AI如何幫你輕松搞定論文重寫?

已經天臨五年了&#xff0c;大學生們還在為論文降重煩惱……手動降重確實是個難題&#xff0c;必須要先付點小經費去靠譜的網站查重&#xff0c;再對著紅字標注去改&#xff0c;后面每一次的論文呢查重結果都像賭//博&#xff0c;誰也不知道明明是同一篇文章&#xff0c;第二次…

2024鯤鵬昇騰創新大賽集訓營Ascend C算子學習筆記

異構計算架構&#xff08;CANN&#xff09; 對標英偉達的CUDA CuDNN的核心軟件層&#xff0c;向上支持多種AI框架&#xff0c;向下服務AI處理器&#xff0c;發揮承上啟下的關鍵作用&#xff0c;是提升昇騰AI處理器計算效率的關鍵平臺。主要包括有各種引擎、編譯器、執行器、算…

(番外篇)指針的一些相關習題講解(速進,干貨滿滿)(2)

前言&#xff1a; 小編感覺最近有點太墮落&#xff0c;于是我開始從事這篇文章的撰寫&#xff0c;現在也是進入七月份了&#xff0c;我現在文章開頭定一個小目標&#xff0c;我決定在七月份發布至少十篇文章&#xff0c;希望我可以說到做到&#xff08;我前面就口頭欠了不少文章…

OpenSSL的一些使用案例

目錄 一、介紹 二、基本使用 1、Shell &#xff08;1&#xff09;文件加解密 &#xff08;2&#xff09;生成密鑰文件 2、API &#xff08;1&#xff09;md5sum &#xff08;2&#xff09;AES256加解密 一、介紹 本篇博客重點不是詳細描述 OpenSSL 的用法&#xff0c;只…

什么是校園氣象站

在科技日新月異的今天&#xff0c;氣象觀測不僅局限于專業的氣象機構&#xff0c;它已經走進了我們的校園&#xff0c;成為了學生們探索自然、學習科學知識的重要平臺。 校園氣象站是設置在學校內部&#xff0c;用于進行氣象觀測、數據記錄和科學實驗的設施。它通常由氣象傳感器…

MySQL之應用層優化和備份與恢復(一)

應用層優化 緩存 作為基礎組件的緩存 緩存有可能成為基礎設施的重要組成部分。也很容易陷入一個陷阱&#xff0c;認為緩存雖然很好用&#xff0c;但并不是重要到非有不可得東西。你也許會辯駁&#xff0c;如果緩存服務器宕機或者緩存被清空&#xff0c;請求也可以直接落在數…

常見鎖策略之可重入鎖VS不可重入鎖

可重入鎖VS不可重入鎖 有一個線程,針對同一把鎖,連續加鎖兩次,如果產生了死鎖,那就是不可重入鎖,如果沒有產生死鎖,那就是可重入鎖. 死鎖 我們之前引入多線程的時候不是講了一個加數字的案例么,我們今天以它來舉例 當我們這樣寫的時候會出現什么問題? 分析:第一個synchron…

前端基礎--Vue3

Vue3基礎 VUE3和VUE2的區別 2020年9月18日&#xff0c;Vue.js發布版3.0版本&#xff0c;代號&#xff1a;One Piece 于 2022 年 2 月 7 日星期一成為新的默認版本! Vue3性能更高,初次渲染快55%, 更新渲染快133% 。體積更小 Vue3.0 打包大小減少41%。 同時Vue3可以更好的支持T…

基于微服務智能推薦健康生活交流平臺的設計與實現(SpringCloud SpringBoot)+文檔

&#x1f497;博主介紹&#x1f497;&#xff1a;?在職Java研發工程師、專注于程序設計、源碼分享、技術交流、專注于Java技術領域和畢業設計? 溫馨提示&#xff1a;文末有 CSDN 平臺官方提供的老師 Wechat / QQ 名片 :) Java精品實戰案例《700套》 2025最新畢業設計選題推薦…