本篇文章給大家?guī)?lái)了關(guān)于Redis的相關(guān)知識(shí),其中主要介紹了持久化的相關(guān)問(wèn)題,可以從幾個(gè)方面介紹一下redis持久化的機(jī)制原理,希望對(duì)大家有幫助。
推薦學(xué)習(xí):Redis教程
本文將從以下幾個(gè)方面介紹Redis持久化機(jī)制:
本文從整體上詳細(xì)介紹Redis的兩種持久化方式,包含工作原理、持久化流程及實(shí)踐策略,以及背后的一些理論知識(shí)。上一篇文章僅介紹了RDB持久化,但是Redis持久化是一個(gè)整體,單獨(dú)介紹不成體系,故重新整理。
Redis是一個(gè)內(nèi)存數(shù)據(jù)庫(kù),所有的數(shù)據(jù)將保存在內(nèi)存中,這與傳統(tǒng)的MySQL、Oracle、SqlServer等關(guān)系型數(shù)據(jù)庫(kù)直接把數(shù)據(jù)保存到硬盤相比,Redis的讀寫效率非常高。但是保存在內(nèi)存中也有一個(gè)很大的缺陷,一旦斷電或者宕機(jī),內(nèi)存數(shù)據(jù)庫(kù)中的內(nèi)容將會(huì)全部丟失。為了彌補(bǔ)這一缺陷,Redis提供了把內(nèi)存數(shù)據(jù)持久化到硬盤文件,以及通過(guò)備份文件來(lái)恢復(fù)數(shù)據(jù)的功能,即Redis持久化機(jī)制。
Redis支持兩種方式的持久化:RDB快照和AOF。
RDB持久化
RDB快照用官方的話來(lái)說(shuō):RDB持久化方案是按照指定時(shí)間間隔對(duì)你的數(shù)據(jù)集生成的時(shí)間點(diǎn)快照(point-to-time snapshot)。它以緊縮的二進(jìn)制文件保存Redis數(shù)據(jù)庫(kù)某一時(shí)刻所有數(shù)據(jù)對(duì)象的內(nèi)存快照,可用于Redis的數(shù)據(jù)備份、轉(zhuǎn)移與恢復(fù)。到目前為止,仍是官方的默認(rèn)支持方案。
RDB工作原理
既然說(shuō)RDB是Redis中數(shù)據(jù)集的時(shí)間點(diǎn)快照,那我們先簡(jiǎn)單了解一下Redis內(nèi)的數(shù)據(jù)對(duì)象在內(nèi)存中是如何存儲(chǔ)與組織的。
默認(rèn)情況下,Redis中有16個(gè)數(shù)據(jù)庫(kù),編號(hào)從0-15,每個(gè)Redis數(shù)據(jù)庫(kù)使用一個(gè)redisDb
對(duì)象來(lái)表示,redisDb
使用hashtable存儲(chǔ)K-V對(duì)象。為方便理解,我以其中一個(gè)db為例繪制Redis內(nèi)部數(shù)據(jù)的存儲(chǔ)結(jié)構(gòu)示意圖。 時(shí)間點(diǎn)快照也就是某一時(shí)刻Redis內(nèi)每個(gè)DB中每個(gè)數(shù)據(jù)對(duì)象的狀態(tài),先假設(shè)在這一時(shí)刻所有的數(shù)據(jù)對(duì)象不再改變,我們就可以按照上圖中的數(shù)據(jù)結(jié)構(gòu)關(guān)系,把這些數(shù)據(jù)對(duì)象依次讀取出來(lái)并寫入到文件中,以此實(shí)現(xiàn)Redis的持久化。然后,當(dāng)Redis重啟時(shí)按照規(guī)則讀取這個(gè)文件中的內(nèi)容,再寫入到Redis內(nèi)存即可恢復(fù)至持久化時(shí)的狀態(tài)。
當(dāng)然,這個(gè)前提時(shí)我們上面的假設(shè)成立,否則面對(duì)一個(gè)時(shí)刻變化的數(shù)據(jù)集,我們無(wú)從下手。我們知道Redis中客戶端命令處理是單線程模型,如果把持久化作為一個(gè)命令處理,那數(shù)據(jù)集肯定時(shí)處于靜止?fàn)顟B(tài)。另外,操作系統(tǒng)提供的fork()函數(shù)創(chuàng)建的子進(jìn)程可獲得與父進(jìn)程一致的內(nèi)存數(shù)據(jù),相當(dāng)于獲取了內(nèi)存數(shù)據(jù)副本;fork完成后,父進(jìn)程該干嘛干嘛,持久化狀態(tài)的工作交給子進(jìn)程就行了。
很顯然,第一種情況不可取,持久化備份會(huì)導(dǎo)致短時(shí)間內(nèi)Redis服務(wù)不可用,這對(duì)于高HA的系統(tǒng)來(lái)講是無(wú)法容忍的。所以,第二種方式是RDB持久化的主要實(shí)踐方式。由于fork子進(jìn)程后,父進(jìn)程數(shù)據(jù)一直在變化,子進(jìn)程并不與父進(jìn)程同步,RDB持久化必然無(wú)法保證實(shí)時(shí)性;RDB持久化完成后發(fā)生斷電或宕機(jī),會(huì)導(dǎo)致部分?jǐn)?shù)據(jù)丟失;備份頻率決定了丟失數(shù)據(jù)量的大小,提高備份頻率,意味著fork過(guò)程消耗較多的CPU資源,也會(huì)導(dǎo)致較大的磁盤I/O。
持久化流程
在Redis內(nèi)完成RDB持久化的方法有rdbSave和rdbSaveBackground兩個(gè)函數(shù)方法(源碼文件rdb.c中),先簡(jiǎn)單說(shuō)下兩者差別:
-
rdbSave:是同步執(zhí)行的,方法調(diào)用后就會(huì)立刻啟動(dòng)持久化流程。由于Redis是單線程模型,持久化過(guò)程中會(huì)阻塞,Redis無(wú)法對(duì)外提供服務(wù);
-
rdbSaveBackground:是后臺(tái)(異步)執(zhí)行的,該方法會(huì)fork出子進(jìn)程,真正的持久化過(guò)程是在子進(jìn)程中執(zhí)行的(調(diào)用rdbSave),主進(jìn)程會(huì)繼續(xù)提供服務(wù);
RDB持久化的觸發(fā)必然離不開(kāi)以上兩個(gè)方法,觸發(fā)的方式分為手動(dòng)和自動(dòng)。手動(dòng)觸發(fā)容易理解,是指我們通過(guò)Redis客戶端人為的對(duì)Redis服務(wù)端發(fā)起持久化備份指令,然后Redis服務(wù)端開(kāi)始執(zhí)行持久化流程,這里的指令有save和bgsave。自動(dòng)觸發(fā)是Redis根據(jù)自身運(yùn)行要求,在滿足預(yù)設(shè)條件時(shí)自動(dòng)觸發(fā)的持久化流程,自動(dòng)觸發(fā)的場(chǎng)景有如下幾個(gè)(摘自這篇文章):
-
serverCron中
save m n
配置規(guī)則自動(dòng)觸發(fā); -
從節(jié)點(diǎn)全量復(fù)制時(shí),主節(jié)點(diǎn)發(fā)送rdb文件給從節(jié)點(diǎn)完成復(fù)制操作,主節(jié)點(diǎn)會(huì)出發(fā)bgsave;
-
執(zhí)行
debug reload
命令重新加載redis時(shí); -
默認(rèn)情況下(未開(kāi)啟AOF)執(zhí)行shutdown命令時(shí),自動(dòng)執(zhí)行bgsave;
結(jié)合源碼及參考文章,我整理了RDB持久化流程來(lái)幫助大家有個(gè)整體的了解,然后再?gòu)囊恍┘?xì)節(jié)進(jìn)行說(shuō)明。 從上圖可以知道:
-
自動(dòng)觸發(fā)的RDB持久化是通過(guò)rdbSaveBackground以子進(jìn)程方式執(zhí)行的持久化策略;
-
手動(dòng)觸發(fā)是以客戶端命令方式觸發(fā)的,包含save和bgsave兩個(gè)命令,其中save命令是在Redis的命令處理線程以阻塞的方式調(diào)用
rdbSave
方法完成的。
自動(dòng)觸發(fā)流程是一個(gè)完整的鏈路,涵蓋了rdbSaveBackground、rdbSave等,接下來(lái)我以serverCron為例分析一下整個(gè)流程。
save規(guī)則及檢查
serverCron是Redis內(nèi)的一個(gè)周期性函數(shù),每隔100毫秒執(zhí)行一次,它的其中一項(xiàng)工作就是:根據(jù)配置文件中save規(guī)則來(lái)判斷當(dāng)前需要進(jìn)行自動(dòng)持久化流程,如果滿足條件則嘗試開(kāi)始持久化。了解一下這部分的實(shí)現(xiàn)。
在redisServer
中有幾個(gè)與RDB持久化有關(guān)的字段,我從代碼中摘出來(lái),中英文對(duì)照著看下:
struct redisServer { /* 省略其他字段 */ /* RDB persistence */ long long dirty; /* Changes to DB from the last save * 上次持久化后修改key的次數(shù) */ struct saveparam *saveparams; /* Save points array for RDB, * 對(duì)應(yīng)配置文件多個(gè)save參數(shù) */ int saveparamslen; /* Number of saving points, * save參數(shù)的數(shù)量 */ time_t lastsave; /* Unix time of last successful save * 上次持久化時(shí)間*/ /* 省略其他字段 */ } /* 對(duì)應(yīng)redis.conf中的save參數(shù) */ struct saveparam { time_t seconds; /* 統(tǒng)計(jì)時(shí)間范圍 */ int changes; /* 數(shù)據(jù)修改次數(shù) */ };
saveparams
對(duì)應(yīng)redis.conf
下的save規(guī)則,save參數(shù)是Redis觸發(fā)自動(dòng)備份的觸發(fā)策略,seconds
為統(tǒng)計(jì)時(shí)間(單位:秒), changes
為在統(tǒng)計(jì)時(shí)間內(nèi)發(fā)生寫入的次數(shù)。save m n
的意思是:m秒內(nèi)有n條寫入就觸發(fā)一次快照,即備份一次。save參數(shù)可以配置多組,滿足在不同條件的備份要求。如果需要關(guān)閉RDB的自動(dòng)備份策略,可以使用save ""
。以下為幾種配置的說(shuō)明:
# 表示900秒(15分鐘)內(nèi)至少有1個(gè)key的值發(fā)生變化,則執(zhí)行 save 900 1 # 表示300秒(5分鐘)內(nèi)至少有1個(gè)key的值發(fā)生變化,則執(zhí)行 save 300 10 # 表示60秒(1分鐘)內(nèi)至少有10000個(gè)key的值發(fā)生變化,則執(zhí)行 save 60 10000 # 該配置將會(huì)關(guān)閉RDB方式的持久化 save ""
serverCron
對(duì)RDB save規(guī)則的檢測(cè)代碼如下所示:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { /* 省略其他邏輯 */ /* 如果用戶請(qǐng)求進(jìn)行AOF文件重寫時(shí),Redis正在執(zhí)行RDB持久化,Redis會(huì)安排在RDB持久化完成后執(zhí)行AOF文件重寫, * 如果aof_rewrite_scheduled為true,說(shuō)明需要執(zhí)行用戶的請(qǐng)求 */ /* Check if a background saving or AOF rewrite in progress terminated. */ if (hasActiveChildProcess() || ldbPendingChildren()) { run_with_period(1000) receiveChildInfo(); checkChildrenDone(); } else { /* 后臺(tái)無(wú) saving/rewrite 子進(jìn)程才會(huì)進(jìn)行,逐個(gè)檢查每個(gè)save規(guī)則*/ for (j = 0; j < server.saveparamslen; j++) { struct saveparam *sp = server.saveparams+j; /* 檢查規(guī)則有幾個(gè):滿足修改次數(shù),滿足統(tǒng)計(jì)周期,達(dá)到重試時(shí)間間隔或者上次持久化完成*/ 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); /* 執(zhí)行bgsave過(guò)程 */ rdbSaveBackground(server.rdb_filename,rsiptr); break; } } /* 省略:Trigger an AOF rewrite if needed. */ } /* 省略其他邏輯 */ }
如果沒(méi)有后臺(tái)的RDB持久化或AOF重寫進(jìn)程,serverCron會(huì)根據(jù)以上配置及狀態(tài)判斷是否需要執(zhí)行持久化操作,判斷依據(jù)就是看lastsave、dirty是否滿足saveparams數(shù)組中的其中一個(gè)條件。如果有一個(gè)條件匹配,則調(diào)用rdbSaveBackground方法,執(zhí)行異步持久化流程。
rdbSaveBackground
rdbSaveBackground是RDB持久化的輔助性方法,主要工作是fork子進(jìn)程,然后根據(jù)調(diào)用方(父進(jìn)程或者子進(jìn)程)不同,有兩種不同的執(zhí)行邏輯。
-
如果調(diào)用方是父進(jìn)程,則fork出子進(jìn)程,保存子進(jìn)程信息后直接返回。
-
如果調(diào)用方是子進(jìn)程則調(diào)用rdbSave執(zhí)行RDB持久化邏輯,持久化完成后退出子進(jìn)程。
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); // fork子進(jìn)程 if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) { int retval; /* Child 子進(jìn)程:修改進(jìn)程標(biāo)題 */ redisSetProcTitle("redis-rdb-bgsave"); redisSetCpuAffinity(server.bgsave_cpulist); // 執(zhí)行rdb持久化 retval = rdbSave(filename,rsi); if (retval == C_OK) { sendChildCOWInfo(CHILD_TYPE_RDB, 1, "RDB"); } // 持久化完成后,退出子進(jìn)程 exitFromChild((retval == C_OK) ? 0 : 1); } else { /* Parent 父進(jìn)程:記錄fork子進(jìn)程的時(shí)間等信息*/ 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); // 記錄子進(jìn)程開(kāi)始的時(shí)間、類型等。 server.rdb_save_time_start = time(NULL); server.rdb_child_type = RDB_CHILD_TYPE_DISK; return C_OK; } return C_OK; /* unreached */ }
rdbSave是真正執(zhí)行持久化的方法,它在執(zhí)行時(shí)存在大量的I/O、計(jì)算操作,耗時(shí)、CPU占用較大,在Redis的單線程模型中持久化過(guò)程會(huì)持續(xù)占用線程資源,進(jìn)而導(dǎo)致Redis無(wú)法提供其他服務(wù)。為了解決這一問(wèn)題Redis在rdbSaveBackground中fork出子進(jìn)程,由子進(jìn)程完成持久化工作,避免了占用父進(jìn)程過(guò)多的資源。
需要注意的是,如果父進(jìn)程內(nèi)存占用過(guò)大,fork過(guò)程會(huì)比較耗時(shí),在這個(gè)過(guò)程中父進(jìn)程無(wú)法對(duì)外提供服務(wù);另外,需要綜合考慮計(jì)算機(jī)內(nèi)存使用量,fork子進(jìn)程后會(huì)占用雙倍的內(nèi)存資源,需要確保內(nèi)存夠用。通過(guò)info stats命令查看latest_fork_usec選項(xiàng),可以獲取最近一個(gè)fork以操作的耗時(shí)。
rdbSave
Redis的rdbSave函數(shù)是真正進(jìn)行RDB持久化的函數(shù),流程、細(xì)節(jié)賊多,整體流程可以總結(jié)為:創(chuàng)建并打開(kāi)臨時(shí)文件、Redis內(nèi)存數(shù)據(jù)寫入臨時(shí)文件、臨時(shí)文件寫入磁盤、臨時(shí)文件重命名為正式RDB文件、更新持久化狀態(tài)信息(dirty、lastsave)。其中“Redis內(nèi)存數(shù)據(jù)寫入臨時(shí)文件”最為核心和復(fù)雜,寫入過(guò)程直接體現(xiàn)了RDB文件的文件格式,本著一圖勝千言的理念,我按照源碼流程繪制了下圖。 補(bǔ)充說(shuō)明一下,上圖右下角“遍歷當(dāng)前數(shù)據(jù)庫(kù)的鍵值對(duì)并寫入”這個(gè)環(huán)節(jié)會(huì)根據(jù)不同類型的Redis數(shù)據(jù)類型及底層數(shù)據(jù)結(jié)構(gòu)采用不同的格式寫入到RDB文件中,不再展開(kāi)了。我覺(jué)得大家對(duì)整個(gè)過(guò)程有個(gè)直觀的理解就好,這對(duì)于我們理解Redis內(nèi)部的運(yùn)作機(jī)制大有裨益。
AOF持久化
上一節(jié)我們知道RDB是一種時(shí)間點(diǎn)(point-to-time)快照,適合數(shù)據(jù)備份及災(zāi)難恢復(fù),由于工作原理的“先天性缺陷”無(wú)法保證實(shí)時(shí)性持久化,這對(duì)于緩存丟失零容忍的系統(tǒng)來(lái)說(shuō)是個(gè)硬傷,于是就有了AOF。
AOF工作原理
AOF是Append Only File的縮寫,它是Redis的完全持久化策略,從1.1版本開(kāi)始支持;這里的file存儲(chǔ)的是引起Redis數(shù)據(jù)修改的命令集合(比如:set/hset/del等),這些集合按照Redis Server的處理順序追加到文件中。當(dāng)重啟Redis時(shí),Redis就可以從頭讀取AOF中的指令并重放,進(jìn)而恢復(fù)關(guān)閉前的數(shù)據(jù)狀態(tài)。
AOF持久化默認(rèn)是關(guān)閉的,修改redis.conf以下信息并重啟,即可開(kāi)啟AOF持久化功能。
# no-關(guān)閉,yes-開(kāi)啟,默認(rèn)no appendonly yes appendfilename appendonly.aof
AOF本質(zhì)是為了持久化,持久化對(duì)象是Redis內(nèi)每一個(gè)key的狀態(tài),持久化的目的是為了在Reids發(fā)生故障重啟后能夠恢復(fù)至重啟前或故障前的狀態(tài)。相比于RDB,AOF采取的策略是按照?qǐng)?zhí)行順序持久化每一條能夠引起Redis中對(duì)象狀態(tài)變更的命令,命令是有序的、有選擇的。把a(bǔ)of文件轉(zhuǎn)移至任何一臺(tái)Redis Server,從頭到尾按序重放這些命令即可恢復(fù)如初。舉個(gè)例子:
首先執(zhí)行指令set number 0
,然后隨機(jī)調(diào)用incr number
、get number
各5次,最后再執(zhí)行一次get number
,我們得到的結(jié)果肯定是5。
因?yàn)樵谶@個(gè)過(guò)程中,能夠引起number
狀態(tài)變更的只有set/incr
類型的指令,并且它們執(zhí)行的先后順序是已知的,無(wú)論執(zhí)行多少次get
都不會(huì)影響number
的狀態(tài)。所以,保留所有set/incr
命令并持久化至aof文件即可。按照aof的設(shè)計(jì)原理,aof文件中的內(nèi)容應(yīng)該是這樣的(這里是假設(shè),實(shí)際為RESP協(xié)議):
set number 0 incr number incr number incr number incr number incr number
最本質(zhì)的原理用“命令重放”四個(gè)字就可以概括。但是,考慮實(shí)際生產(chǎn)環(huán)境的復(fù)雜性及操作系統(tǒng)等方面的限制,Redis所要考慮的工作要比這個(gè)例子復(fù)雜的多:
-
Redis Server啟動(dòng)后,aof文件一直在追加命令,文件會(huì)越來(lái)越大。文件越大,Redis重啟后恢復(fù)耗時(shí)越久;文件太大,轉(zhuǎn)移工作就越難;不加管理,可能撐爆硬盤。很顯然,需要在合適的時(shí)機(jī)對(duì)文件進(jìn)行精簡(jiǎn)。例子中的5條incr指令很明顯的可以替換為為一條
set
命令,存在很大的壓縮空間。 -
眾所周知,文件I/O是操作系統(tǒng)性能的短板,為了提高效率,文件系統(tǒng)設(shè)計(jì)了一套復(fù)雜的緩存機(jī)制,Redis操作命令的追加操作只是把數(shù)據(jù)寫入了緩沖區(qū)(aof_buf),從緩沖區(qū)到寫入物理文件在性能與安全之間權(quán)衡會(huì)有不同的選擇。
-
文件壓縮即意味著重寫,重寫時(shí)即可依據(jù)已有的aof文件做命令整合,也可以先根據(jù)當(dāng)前Redis內(nèi)數(shù)據(jù)的狀態(tài)做快照,再把存儲(chǔ)快照過(guò)程中的新增的命令做追加。
-
aof備份后的文件是為了恢復(fù)數(shù)據(jù),結(jié)合aof文件的格式、完整性等因素,Redis也要設(shè)計(jì)一套完整的方案做支持。
持久化流程
從流程上來(lái)看,AOF的工作原理可以概括為幾個(gè)步驟:命令追加(append)、文件寫入與同步(fsync)、文件重寫(rewrite)、重啟加載(load),接下來(lái)依次了解每個(gè)步驟的細(xì)節(jié)及背后的設(shè)計(jì)哲學(xué)。
命令追加
當(dāng) AOF 持久化功能處于打開(kāi)狀態(tài)時(shí),Redis 在執(zhí)行完一個(gè)寫命令之后,會(huì)以協(xié)議格式(也就是RESP,即 Redis 客戶端和服務(wù)器交互的通信協(xié)議 )把被執(zhí)行的寫命令追加到 Redis 服務(wù)端維護(hù)的 AOF 緩沖區(qū)末尾。對(duì)AOF文件只有單線程的追加操作,沒(méi)有seek等復(fù)雜的操作,即使斷電或宕機(jī)也不存在文件損壞風(fēng)險(xiǎn)。另外,使用文本協(xié)議好處多多:
-
文本協(xié)議有很好的兼容性;
-
文本協(xié)議就是客戶端的請(qǐng)求命令,不需要二次處理,節(jié)省了存儲(chǔ)及加載時(shí)的處理開(kāi)銷;
-
文本協(xié)議具有可讀性,方便查看、修改等處理。
AOF緩沖區(qū)類型為Redis自主設(shè)計(jì)的數(shù)據(jù)結(jié)構(gòu)sds
,Redis會(huì)根據(jù)命令的類型采用不同的方法(catAppendOnlyGenericCommand
、catAppendOnlyExpireAtCommand
等)對(duì)命令內(nèi)容進(jìn)行處理,最后寫入緩沖區(qū)。
需要注意的是:如果命令追加時(shí)正在進(jìn)行AOF重寫,這些命令還會(huì)追加到重寫緩沖區(qū)(aof_rewrite_buffer
)。
文件寫入與同步
AOF文件的寫入與同步離不開(kāi)操作系統(tǒng)的支持,開(kāi)始介紹之前,我們需要補(bǔ)充一下Linux I/O緩沖區(qū)相關(guān)知識(shí)。硬盤I/O性能較差,文件讀寫速度遠(yuǎn)遠(yuǎn)比不上CPU的處理速度,如果每次文件寫入都等待數(shù)據(jù)寫入硬盤,會(huì)整體拉低操作系統(tǒng)的性能。為了解決這個(gè)問(wèn)題,操作系統(tǒng)提供了延遲寫(delayed write)機(jī)制來(lái)提高硬盤的I/O性能。
傳統(tǒng)的UNIX實(shí)現(xiàn)在內(nèi)核中設(shè)有緩沖區(qū)高速緩存或頁(yè)面高速緩存,大多數(shù)磁盤I/O都通過(guò)緩沖進(jìn)行。 當(dāng)將數(shù)據(jù)寫入文件時(shí),內(nèi)核通常先將該數(shù)據(jù)復(fù)制到其中一個(gè)緩沖區(qū)中,如果該緩沖區(qū)尚未寫滿,則并不將其排入輸出隊(duì)列,而是等待其寫滿或者當(dāng)內(nèi)核需要重用該緩沖區(qū)以便存放其他磁盤塊數(shù)據(jù)時(shí), 再將該緩沖排入到輸出隊(duì)列,然后待其到達(dá)隊(duì)首時(shí),才進(jìn)行實(shí)際的I/O操作。這種輸出方式就被稱為延遲寫。
延遲寫減少了磁盤讀寫次數(shù),但是卻降低了文件內(nèi)容的更新速度,使得欲寫到文件中的數(shù)據(jù)在一段時(shí)間內(nèi)并沒(méi)有寫到磁盤上。當(dāng)系統(tǒng)發(fā)生故障時(shí),這種延遲可能造成文件更新內(nèi)容的丟失。為了保證磁盤上實(shí)際文件系統(tǒng)與緩沖區(qū)高速緩存中內(nèi)容的一致性,UNIX系統(tǒng)提供了sync、fsync和fdatasync三個(gè)函數(shù)為強(qiáng)制寫入硬盤提供支持。
Redis每次事件輪訓(xùn)結(jié)束前(beforeSleep
)都會(huì)調(diào)用函數(shù)flushAppendOnlyFile
,flushAppendOnlyFile
會(huì)把AOF緩沖區(qū)(aof_buf
)中的數(shù)據(jù)寫入內(nèi)核緩沖區(qū),并且根據(jù)appendfsync
配置來(lái)決定采用何種策略把內(nèi)核緩沖區(qū)中的數(shù)據(jù)寫入磁盤,即調(diào)用fsync()
。該配置有三個(gè)可選項(xiàng)always
、no
、everysec
,具體說(shuō)明如下:
-
always:每次都調(diào)用
fsync()
,是安全性最高、性能最差的一種策略。 -
no:不會(huì)調(diào)用
fsync()
。性能最好,安全性最差。 -
everysec:僅在滿足同步條件時(shí)調(diào)用
fsync()
。這是官方建議的同步策略,也是默認(rèn)配置,做到兼顧性能和數(shù)據(jù)安全性,理論上只有在系統(tǒng)突然宕機(jī)的情況下丟失1秒的數(shù)據(jù)。
注意:上面介紹的策略受配置項(xiàng)no-appendfsync-on-rewrite
的影響,它的作用是告知Redis:AOF文件重寫期間是否禁止調(diào)用fsync(),默認(rèn)是no。
如果appendfsync
設(shè)置為always
或everysec
,后臺(tái)正在進(jìn)行的BGSAVE
或者BGREWRITEAOF
消耗過(guò)多的磁盤I/O,在某些Linux系統(tǒng)配置下,Redis對(duì)fsync()的調(diào)用可能阻塞很長(zhǎng)時(shí)間。然而這個(gè)問(wèn)題還沒(méi)有修復(fù),因?yàn)榧词故窃诓煌木€程中執(zhí)行fsync()
,同步寫入操作也會(huì)被阻塞。
為了緩解此問(wèn)題,可以使用該選項(xiàng),以防止在進(jìn)行BGSAVE
或BGREWRITEAOF
時(shí)在主進(jìn)程中調(diào)用fsync()。
-
設(shè)置為
yes
意味著,如果子進(jìn)程正在進(jìn)行BGSAVE
或BGREWRITEAOF
,AOF的持久化能力就與appendfsync
設(shè)置為no
有著相同的效果。最糟糕的情況下,這可能會(huì)導(dǎo)致30秒的緩存數(shù)據(jù)丟失。 -
如果你的系統(tǒng)有上面描述的延遲問(wèn)題,就把這個(gè)選項(xiàng)設(shè)置為
yes
,否則保持為no
。
文件重寫
如前面提到的,Redis長(zhǎng)時(shí)間運(yùn)行,命令不斷寫入AOF,文件會(huì)越來(lái)越大,不加控制可能影響宿主機(jī)的安全。
為了解決AOF文件體積問(wèn)題,Redis引入了AOF文件重寫功能,它會(huì)根據(jù)Redis內(nèi)數(shù)據(jù)對(duì)象的最新?tīng)顟B(tài)生成新的AOF文件,新舊文件對(duì)應(yīng)的數(shù)據(jù)狀態(tài)一致,但是新文件會(huì)具有較小的體積。重寫既減少了AOF文件對(duì)磁盤空間的占用,又可以提高Redis重啟時(shí)數(shù)據(jù)恢復(fù)的速度。還是下面這個(gè)例子,舊文件中的6條命令等同于新文件中的1條命令,壓縮效果顯而易見(jiàn)。 我們說(shuō),AOF文件太大時(shí)會(huì)觸發(fā)AOF文件重寫,那到底是多大呢?有哪些情況會(huì)觸發(fā)重寫操作呢? ** 與RDB方式一樣,AOF文件重寫既可以手動(dòng)觸發(fā),也會(huì)自動(dòng)觸發(fā)。手動(dòng)觸發(fā)直接調(diào)用
bgrewriteaof
命令,如果當(dāng)時(shí)無(wú)子進(jìn)程執(zhí)行會(huì)立刻執(zhí)行,否則安排在子進(jìn)程結(jié)束后執(zhí)行。自動(dòng)觸發(fā)由Redis的周期性方法serverCron
檢查在滿足一定條件時(shí)觸發(fā)。先了解兩個(gè)配置項(xiàng):
-
auto-aof-rewrite-percentage:代表當(dāng)前AOF文件大?。╝of_current_size)和上一次重寫后AOF文件大?。╝of_base_size)相比,增長(zhǎng)的比例。
-
auto-aof-rewrite-min-size:表示運(yùn)行
BGREWRITEAOF
時(shí)AOF文件占用空間最小值,默認(rèn)為64MB;
Redis啟動(dòng)時(shí)把aof_base_size
初始化為當(dāng)時(shí)aof文件的大小,Redis運(yùn)行過(guò)程中,當(dāng)AOF文件重寫操作完成時(shí),會(huì)對(duì)其進(jìn)行更新;aof_current_size
為serverCron
執(zhí)行時(shí)AOF文件的實(shí)時(shí)大小。當(dāng)滿足以下兩個(gè)條件時(shí),AOF文件重寫就會(huì)觸發(fā):
增長(zhǎng)比例:(aof_current_size - aof_base_size) / aof_base_size > auto-aof-rewrite-percentage 文件大?。篴of_current_size > auto-aof-rewrite-min-size
手動(dòng)觸發(fā)與自動(dòng)觸發(fā)的代碼如下,同樣在周期性方法serverCron
中:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { /* 省略其他邏輯 */ /* 如果用戶請(qǐng)求進(jìn)行AOF文件重寫時(shí),Redis正在執(zhí)行RDB持久化,Redis會(huì)安排在RDB持久化完成后執(zhí)行AOF文件重寫, * 如果aof_rewrite_scheduled為true,說(shuō)明需要執(zhí)行用戶的請(qǐng)求 */ if (!hasActiveChildProcess() && server.aof_rewrite_scheduled) { rewriteAppendOnlyFileBackground(); } /* Check if a background saving or AOF rewrite in progress terminated. */ if (hasActiveChildProcess() || ldbPendingChildren()) { run_with_period(1000) receiveChildInfo(); checkChildrenDone(); } else { /* 省略rdb持久化條件檢查 */ /* AOF重寫條件檢查:aof開(kāi)啟、無(wú)子進(jìn)程運(yùn)行、增長(zhǎng)百分比已設(shè)置、當(dāng)前文件大小超過(guò)閾值 */ if (server.aof_state == AOF_ON && !hasActiveChildProcess() && server.aof_rewrite_perc && server.aof_current_size > server.aof_rewrite_min_size) { long long base = server.aof_rewrite_base_size ? server.aof_rewrite_base_size : 1; /* 計(jì)算增長(zhǎng)百分比 */ long long growth = (server.aof_current_size*100/base) - 100; if (growth >= server.aof_rewrite_perc) { serverLog(LL_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth); rewriteAppendOnlyFileBackground(); } } } /**/ }
AOF文件重寫的流程是什么?聽(tīng)說(shuō)Redis支持混合持久化,對(duì)AOF文件重寫有什么影響?
從4.0版本開(kāi)始,Redis在AOF模式中引入了混合持久化方案,即:純AOF方式、RDB+AOF方式,這一策略由配置參數(shù)aof-use-rdb-preamble
(使用RDB作為AOF文件的前半段)控制,默認(rèn)關(guān)閉(no),設(shè)置為yes可開(kāi)啟。所以,在AOF重寫過(guò)程中文件的寫入會(huì)有兩種不同的方式。當(dāng)aof-use-rdb-preamble
的值是:
-
no:按照AOF格式寫入命令,與4.0前版本無(wú)差別;
-
yes:先按照RDB格式寫入數(shù)據(jù)狀態(tài),然后把重寫期間AOF緩沖區(qū)的內(nèi)容以AOF格式寫入,文件前半部分為RDB格式,后半部分為AOF格式。
結(jié)合源碼(6.0版本,源碼太多這里不貼出,可參考aof.c
)及參考資料,繪制AOF重寫(BGREWRITEAOF)流程圖: 結(jié)合上圖,總結(jié)一下AOF文件重寫的流程:
-
rewriteAppendOnlyFileBackground開(kāi)始執(zhí)行,檢查是否有正在進(jìn)行的AOF重寫或RDB持久化子進(jìn)程:如果有,則退出該流程;如果沒(méi)有,則繼續(xù)創(chuàng)建接下來(lái)父子進(jìn)程間數(shù)據(jù)傳輸?shù)耐ㄐ殴艿?。?zhí)行fork()操作,成功后父子進(jìn)程分別執(zhí)行不同的流程。
- 父進(jìn)程:
-
記錄子進(jìn)程信息(pid)、時(shí)間戳等;
-
繼續(xù)響應(yīng)其他客戶端請(qǐng)求;
-
收集AOF重寫期間的命令,追加至aof_rewrite_buffer;
-
等待并向子進(jìn)程同步aof_rewrite_buffer的內(nèi)容;
-
- 子進(jìn)程:
-
修改當(dāng)前進(jìn)程名稱,創(chuàng)建重寫所需的臨時(shí)文件,調(diào)用rewriteAppendOnlyFile函數(shù);
-
根據(jù)
aof-use-rdb-preamble
配置,以RDB或AOF方式寫入前半部分,并同步至硬盤; -
從父進(jìn)程接收增量AOF命令,以AOF方式寫入后半部分,并同步至硬盤;
-
重命名AOF文件,子進(jìn)程退出。
-
數(shù)據(jù)加載
Redis啟動(dòng)后通過(guò)loadDataFromDisk
函數(shù)執(zhí)行數(shù)據(jù)加載工作。這里需要注意,雖然持久化方式可以選擇AOF、RDB或者兩者兼用,但是數(shù)據(jù)加載時(shí)必須做出選擇,兩種方式各自加載一遍就亂套了。
理論上,AOF持久化比RDB具有更好的實(shí)時(shí)性,當(dāng)開(kāi)啟了AOF持久化方式,Redis在數(shù)據(jù)加載時(shí)優(yōu)先考慮AOF方式。而且,Redis 4.0版本后AOF支持了混合持久化,加載AOF文件需要考慮版本兼容性。Redis數(shù)據(jù)加載流程如下圖所示: 在AOF方式下,開(kāi)啟混合持久化機(jī)制生成的文件是“RDB頭+AOF尾”,未開(kāi)啟時(shí)生成的文件全部為AOF格式??紤]兩種文件格式的兼容性,如果Redis發(fā)現(xiàn)AOF文件為RDB頭,會(huì)使用RDB數(shù)據(jù)加載的方法讀取并恢復(fù)前半部分;然后再使用AOF方式讀取并恢復(fù)后半部分。由于AOF格式存儲(chǔ)的數(shù)據(jù)為RESP協(xié)議命令,Redis采用偽客戶端執(zhí)行命令的方式來(lái)恢復(fù)數(shù)據(jù)。
如果在AOF命令追加過(guò)程中發(fā)生宕機(jī),由于延遲寫的技術(shù)特點(diǎn),AOF的RESP命令可能不完整(被截?cái)啵S龅竭@種情況時(shí),Redis會(huì)按照配置項(xiàng)aof-load-truncated
執(zhí)行不同的處理策略。這個(gè)配置是告訴Redis啟動(dòng)時(shí)讀取aof文件,如果發(fā)現(xiàn)文件被截?cái)啵ú煌暾r(shí)該如何處理:
-
yes:則盡可能多的加載數(shù)據(jù),并以日志的方式通知用戶;
-
no:則以系統(tǒng)錯(cuò)誤的方式崩潰,并禁止啟動(dòng),需要用戶修復(fù)文件后再重啟。
總結(jié)
Redis提供了兩種持久化的選擇:RDB支持以特定的實(shí)踐間隔為數(shù)據(jù)集生成時(shí)間點(diǎn)快照;AOF把Redis Server收到的每條寫指令持久化到日志中,待Redis重啟時(shí)通過(guò)重放命令恢復(fù)數(shù)據(jù)。日志格式為RESP協(xié)議,對(duì)日志文件只做append操作,無(wú)損壞風(fēng)險(xiǎn)。并且當(dāng)AOF文件過(guò)大時(shí)可以自動(dòng)重寫壓縮文件。
當(dāng)然,如果你不需要對(duì)數(shù)據(jù)進(jìn)行持久化,也可以禁用Redis的持久化功能,但是大多數(shù)情況并非如此。實(shí)際上,我們時(shí)有可能同時(shí)使用RDB和AOF兩種方式的,最重要的就是我們要理解兩者的區(qū)別,以便合理使用。
RDB vs AOF
RDB優(yōu)點(diǎn)
-
RDB是一個(gè)緊湊壓縮的二進(jìn)制文件,代表Redis在某一個(gè)時(shí)間點(diǎn)上的數(shù)據(jù)快照,非常適合用于備份、全量復(fù)制等場(chǎng)景。
-
RDB對(duì)災(zāi)難恢復(fù)、數(shù)據(jù)遷移非常友好,RDB文件可以轉(zhuǎn)移至任何需要的地方并重新加載。
-
RDB是Redis數(shù)據(jù)的內(nèi)存快照,數(shù)據(jù)恢復(fù)速度較快,相比于AOF的命令重放有著更高的性能。
RDB缺點(diǎn)
-
RDB方式無(wú)法做到實(shí)時(shí)或秒級(jí)持久化。因?yàn)槌志没^(guò)程是通過(guò)fork子進(jìn)程后由子進(jìn)程完成的,子進(jìn)程的內(nèi)存只是在fork操作那一時(shí)刻父進(jìn)程的數(shù)據(jù)快照,而fork操作后父進(jìn)程持續(xù)對(duì)外服務(wù),內(nèi)部數(shù)據(jù)時(shí)刻變更,子進(jìn)程的數(shù)據(jù)不再更新,兩者始終存在差異,所以無(wú)法做到實(shí)時(shí)性。
-
RDB持久化過(guò)程中的fork操作,會(huì)導(dǎo)致內(nèi)存占用加倍,而且父進(jìn)程數(shù)據(jù)越多,fork過(guò)程越長(zhǎng)。
-
Redis請(qǐng)求高并發(fā)可能會(huì)頻繁命中save規(guī)則,導(dǎo)致fork操作及持久化備份的頻率不可控;
-
RDB文件有文件格式要求,不同版本的Redis會(huì)對(duì)文件格式進(jìn)行調(diào)整,存在老版本無(wú)法兼容新版本的問(wèn)題。
AOF優(yōu)點(diǎn)
-
AOF持久化有更好的實(shí)時(shí)性,我們可以選擇三種不同的方式(appendfsync):no、every second、always,every second作為默認(rèn)的策略具有最好的性能,極端情況下可能會(huì)丟失一秒的數(shù)據(jù)。
-
AOF文件只有append操作,無(wú)復(fù)雜的seek等文件操作,沒(méi)有損壞風(fēng)險(xiǎn)。即使最后寫入數(shù)據(jù)被截?cái)?,也很容易使?code>redis-check-aof工具修復(fù);
-
當(dāng)AOF文件變大時(shí),Redis可在后臺(tái)自動(dòng)重寫。重寫過(guò)程中舊文件會(huì)持續(xù)寫入,重寫完成后新文件將變得更小,并且重寫過(guò)程中的增量命令也會(huì)append到新文件。
-
AOF文件以已于理解與解析的方式包含了對(duì)Redis中數(shù)據(jù)的所有操作命令。即使不小心錯(cuò)誤的清除了所有數(shù)據(jù),只要沒(méi)有對(duì)AOF文件重寫,我們就可以通過(guò)移除最后一條命令找回所有數(shù)據(jù)。
-
AOF已經(jīng)支持混合持久化,文件大小可以有效控制,并提高了數(shù)據(jù)加載時(shí)的效率。
AOF缺點(diǎn)
-
對(duì)于相同的數(shù)據(jù)集合,AOF文件通常會(huì)比RDB文件大;
-
在特定的fsync策略下,AOF會(huì)比RDB略慢。一般來(lái)講,fsync_every_second的性能仍然很高,fsync_no的性能與RDB相當(dāng)。但是在巨大的寫壓力下,RDB更能提供最大的低延時(shí)保障。
-
在AOF上,Redis曾經(jīng)遇到一些幾乎不可能在RDB上遇到的罕見(jiàn)bug。一些特殊的指令(如BRPOPLPUSH)導(dǎo)致重新加載的數(shù)據(jù)與持久化之前不一致,Redis官方曾經(jīng)在相同的條件下進(jìn)行測(cè)試,但是無(wú)法復(fù)現(xiàn)問(wèn)題。
使用建議
對(duì)RDB和AOF兩種持久化方式的工作原理、執(zhí)行流程及優(yōu)缺點(diǎn)了解后,我們來(lái)思考下,實(shí)際場(chǎng)景中應(yīng)該怎么權(quán)衡利弊,合理的使用兩種持久化方式。如果僅僅是使用Redis作為緩存工具,所有數(shù)據(jù)可以根據(jù)持久化數(shù)據(jù)庫(kù)進(jìn)行重建,則可關(guān)閉持久化功能,做好預(yù)熱、緩存穿透、擊穿、雪崩之類的防護(hù)工作即可。
一般情況下,Redis會(huì)承擔(dān)