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