微信搜索公衆號「碼路印記」
本文將從如下幾個方面介紹Redis持久化機制:html
## 寫在前面面試
本文從總體上詳細介紹Redis的兩種持久化方式,包含工做原理、持久化流程及實踐策略,以及背後的一些理論知識。上一篇文章僅介紹了RDB持久化,可是Redis持久化是一個總體,單獨介紹不成體系,故從新整理。
Redis是一個內存數據庫,全部的數據將保存在內存中,這與傳統的MySQL、Oracle、SqlServer等關係型數據庫直接把數據保存到硬盤相比,Redis的讀寫效率很是高。可是保存在內存中也有一個很大的缺陷,一旦斷電或者宕機,內存數據庫中的內容將會所有丟失。爲了彌補這一缺陷,Redis提供了把內存數據持久化到硬盤文件,以及經過備份文件來恢復數據的功能,即Redis持久化機制。redis
Redis支持兩種方式的持久化:RDB快照和AOF。shell
RDB快照用官方的話來講:RDB持久化方案是按照指定時間間隔對你的數據集生成的時間點快照(point-to-time snapshot)。它以緊縮的二進制文件保存Redis數據庫某一時刻全部數據對象的內存快照,可用於Redis的數據備份、轉移與恢復。到目前爲止,還是官方的默認支持方案。數據庫
既然說RDB是Redis中數據集的時間點快照,那咱們先簡單瞭解一下Redis內的數據對象在內存中是如何存儲與組織的。數組
默認狀況下,Redis中有16個數據庫,編號從0-15,每一個Redis數據庫使用一個redisDb
對象來表示,redisDb
使用hashtable存儲K-V對象。爲方便理解,我以其中一個db爲例繪製Redis內部數據的存儲結構示意圖。
時間點快照也就是某一時刻Redis內每一個DB中每一個數據對象的狀態,先假設在這一時刻全部的數據對象再也不改變,咱們就能夠按照上圖中的數據結構關係,把這些數據對象依次讀取出來並寫入到文件中,以此實現Redis的持久化。而後,當Redis重啓時按照規則讀取這個文件中的內容,再寫入到Redis內存便可恢復至持久化時的狀態。緩存
固然,這個前提時咱們上面的假設成立,不然面對一個時刻變化的數據集,咱們無從下手。咱們知道Redis中客戶端命令處理是單線程模型,若是把持久化做爲一個命令處理,那數據集確定時處於靜止狀態。另外,操做系統提供的fork()函數建立的子進程可得到與父進程一致的內存數據,至關於獲取了內存數據副本;fork完成後,父進程該幹嗎幹嗎,持久化狀態的工做交給子進程就好了。安全
很顯然,第一種狀況不可取,持久化備份會致使短期內Redis服務不可用,這對於高HA的系統來說是沒法容忍的。因此,第二種方式是RDB持久化的主要實踐方式。因爲fork子進程後,父進程數據一直在變化,子進程並不與父進程同步,RDB持久化必然沒法保證明時性;RDB持久化完成後發生斷電或宕機,會致使部分數據丟失;備份頻率決定了丟失數據量的大小,提升備份頻率,意味着fork過程消耗較多的CPU資源,也會致使較大的磁盤I/O。服務器
在Redis內完成RDB持久化的方法有rdbSave和rdbSaveBackground兩個函數方法(源碼文件rdb.c中),先簡單說下二者差異:微信
RDB持久化的觸發必然離不開以上兩個方法,觸發的方式分爲手動和自動。手動觸發容易理解,是指咱們經過Redis客戶端人爲的對Redis服務端發起持久化備份指令,而後Redis服務端開始執行持久化流程,這裏的指令有save和bgsave。自動觸發是Redis根據自身運行要求,在知足預設條件時自動觸發的持久化流程,自動觸發的場景有以下幾個(摘自這篇文章):
save m n
配置規則自動觸發;debug reload
命令從新加載redis時;結合源碼及參考文章,我整理了RDB持久化流程來幫助你們有個總體的瞭解,而後再從一些細節進行說明。
從上圖能夠知道:
rdbSave
方法完成的。自動觸發流程是一個完整的鏈路,涵蓋了rdbSaveBackground、rdbSave等,接下來我以serverCron爲例分析一下整個流程。
serverCron是Redis內的一個週期性函數,每隔100毫秒執行一次,它的其中一項工做就是:根據配置文件中save規則來判斷當前須要進行自動持久化流程,若是知足條件則嘗試開始持久化。瞭解一下這部分的實現。
在redisServer
中有幾個與RDB持久化有關的字段,我從代碼中摘出來,中英文對照着看下:
struct redisServer { /* 省略其餘字段 */ /* RDB persistence */ long long dirty; /* Changes to DB from the last save * 上次持久化後修改key的次數 */ struct saveparam *saveparams; /* Save points array for RDB, * 對應配置文件多個save參數 */ int saveparamslen; /* Number of saving points, * save參數的數量 */ time_t lastsave; /* Unix time of last successful save * 上次持久化時間*/ /* 省略其餘字段 */ } /* 對應redis.conf中的save參數 */ struct saveparam { time_t seconds; /* 統計時間範圍 */ int changes; /* 數據修改次數 */ };
saveparams
對應redis.conf
下的save規則,save參數是Redis觸發自動備份的觸發策略,seconds
爲統計時間(單位:秒), changes
爲在統計時間內發生寫入的次數。save m n
的意思是:m秒內有n條寫入就觸發一次快照,即備份一次。save參數能夠配置多組,知足在不一樣條件的備份要求。若是須要關閉RDB的自動備份策略,可使用save ""
。如下爲幾種配置的說明:
# 表示900秒(15分鐘)內至少有1個key的值發生變化,則執行 save 900 1 # 表示300秒(5分鐘)內至少有1個key的值發生變化,則執行 save 300 10 # 表示60秒(1分鐘)內至少有10000個key的值發生變化,則執行 save 60 10000 # 該配置將會關閉RDB方式的持久化 save ""
serverCron
對RDB save規則的檢測代碼以下所示:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { /* 省略其餘邏輯 */ /* 若是用戶請求進行AOF文件重寫時,Redis正在執行RDB持久化,Redis會安排在RDB持久化完成後執行AOF文件重寫, * 若是aof_rewrite_scheduled爲true,說明須要執行用戶的請求 */ /* Check if a background saving or AOF rewrite in progress terminated. */ if (hasActiveChildProcess() || ldbPendingChildren()) { run_with_period(1000) receiveChildInfo(); checkChildrenDone(); } else { /* 後臺無 saving/rewrite 子進程纔會進行,逐個檢查每一個save規則*/ for (j = 0; j < server.saveparamslen; j++) { struct saveparam *sp = server.saveparams+j; /* 檢查規則有幾個:知足修改次數,知足統計週期,達到重試時間間隔或者上次持久化完成*/ 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); /* 執行bgsave過程 */ rdbSaveBackground(server.rdb_filename,rsiptr); break; } } /* 省略:Trigger an AOF rewrite if needed. */ } /* 省略其餘邏輯 */ }
若是沒有後臺的RDB持久化或AOF重寫進程,serverCron會根據以上配置及狀態判斷是否須要執行持久化操做,判斷依據就是看lastsave、dirty是否知足saveparams數組中的其中一個條件。若是有一個條件匹配,則調用rdbSaveBackground方法,執行異步持久化流程。
rdbSaveBackground是RDB持久化的輔助性方法,主要工做是fork子進程,而後根據調用方(父進程或者子進程)不一樣,有兩種不一樣的執行邏輯。
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); // 執行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是真正執行持久化的方法,它在執行時存在大量的I/O、計算操做,耗時、CPU佔用較大,在Redis的單線程模型中持久化過程會持續佔用線程資源,進而致使Redis沒法提供其餘服務。爲了解決這一問題Redis在rdbSaveBackground中fork出子進程,由子進程完成持久化工做,避免了佔用父進程過多的資源。
須要注意的是,若是父進程內存佔用過大,fork過程會比較耗時,在這個過程當中父進程沒法對外提供服務;另外,須要綜合考慮計算機內存使用量,fork子進程後會佔用雙倍的內存資源,須要確保內存夠用。經過info stats命令查看latest_fork_usec選項,能夠獲取最近一個fork以操做的耗時。
Redis的rdbSave函數是真正進行RDB持久化的函數,流程、細節賊多,總體流程能夠總結爲:建立並打開臨時文件、Redis內存數據寫入臨時文件、臨時文件寫入磁盤、臨時文件重命名爲正式RDB文件、更新持久化狀態信息(dirty、lastsave)。其中「Redis內存數據寫入臨時文件」最爲核心和複雜,寫入過程直接體現了RDB文件的文件格式,本着一圖勝千言的理念,我按照源碼流程繪製了下圖。
補充說明一下,上圖右下角「遍歷當前數據庫的鍵值對並寫入」這個環節會根據不一樣類型的Redis數據類型及底層數據結構採用不一樣的格式寫入到RDB文件中,再也不展開了。我以爲你們對整個過程有個直觀的理解就好,這對於咱們理解Redis內部的運做機制大有裨益。
上一節咱們知道RDB是一種時間點(point-to-time)快照,適合數據備份及災難恢復,因爲工做原理的「先天性缺陷」沒法保證明時性持久化,這對於緩存丟失零容忍的系統來講是個硬傷,因而就有了AOF。
AOF是Append Only File的縮寫,它是Redis的徹底持久化策略,從1.1版本開始支持;這裏的file存儲的是引發Redis數據修改的命令集合(好比:set/hset/del等),這些集合按照Redis Server的處理順序追加到文件中。當重啓Redis時,Redis就能夠從頭讀取AOF中的指令並重放,進而恢復關閉前的數據狀態。
AOF持久化默認是關閉的,修改redis.conf如下信息並重啓,便可開啓AOF持久化功能。
# no-關閉,yes-開啓,默認no appendonly yes appendfilename appendonly.aof
AOF本質是爲了持久化,持久化對象是Redis內每個key的狀態,持久化的目的是爲了在Reids發生故障重啓後可以恢復至重啓前或故障前的狀態。相比於RDB,AOF採起的策略是按照執行順序持久化每一條可以引發Redis中對象狀態變動的命令,命令是有序的、有選擇的。把aof文件轉移至任何一臺Redis Server,從頭至尾按序重放這些命令便可恢復如初。舉個例子:
首先執行指令set number 0
,而後隨機調用incr number
、get number
各5次,最後再執行一次get number
,咱們獲得的結果確定是5。
由於在這個過程當中,可以引發number
狀態變動的只有set/incr
類型的指令,而且它們執行的前後順序是已知的,不管執行多少次get
都不會影響number
的狀態。因此,保留全部set/incr
命令並持久化至aof文件便可。按照aof的設計原理,aof文件中的內容應該是這樣的(這裏是假設,實際爲RESP協議):
set number 0 incr number incr number incr number incr number incr number
最本質的原理用「命令重放」四個字就能夠歸納。可是,考慮實際生產環境的複雜性及操做系統等方面的限制,Redis所要考慮的工做要比這個例子複雜的多:
set
命令,存在很大的壓縮空間。從流程上來看,AOF的工做原理能夠歸納爲幾個步驟:命令追加(append)、文件寫入與同步(fsync)、文件重寫(rewrite)、重啓加載(load),接下來依次瞭解每一個步驟的細節及背後的設計哲學。
當 AOF 持久化功能處於打開狀態時,Redis 在執行完一個寫命令以後,會以協議格式(也就是RESP,即 Redis 客戶端和服務器交互的通訊協議 )把被執行的寫命令追加到 Redis 服務端維護的 AOF 緩衝區末尾。對AOF文件只有單線程的追加操做,沒有seek等複雜的操做,即便斷電或宕機也不存在文件損壞風險。另外,使用文本協議好處多多:
AOF緩衝區類型爲Redis自主設計的數據結構sds
,Redis會根據命令的類型採用不一樣的方法(catAppendOnlyGenericCommand
、catAppendOnlyExpireAtCommand
等)對命令內容進行處理,最後寫入緩衝區。
須要注意的是:若是命令追加時正在進行AOF重寫,這些命令還會追加到重寫緩衝區(aof_rewrite_buffer
)。
AOF文件的寫入與同步離不開操做系統的支持,開始介紹以前,咱們須要補充一下Linux I/O緩衝區相關知識。硬盤I/O性能較差,文件讀寫速度遠遠比不上CPU的處理速度,若是每次文件寫入都等待數據寫入硬盤,會總體拉低操做系統的性能。爲了解決這個問題,操做系統提供了延遲寫(delayed write)機制來提升硬盤的I/O性能。
傳統的UNIX實如今內核中設有緩衝區高速緩存或頁面高速緩存,大多數磁盤I/O都經過緩衝進行。 當將數據寫入文件時,內核一般先將該數據複製到其中一個緩衝區中,若是該緩衝區還沒有寫滿,則並不將其排入輸出隊列,而是等待其寫滿或者當內核須要重用該緩衝區以便存放其餘磁盤塊數據時, 再將該緩衝排入到輸出隊列,而後待其到達隊首時,才進行實際的I/O操做。這種輸出方式就被稱爲延遲寫。
延遲寫減小了磁盤讀寫次數,可是卻下降了文件內容的更新速度,使得欲寫到文件中的數據在一段時間內並無寫到磁盤上。當系統發生故障時,這種延遲可能形成文件更新內容的丟失。爲了保證磁盤上實際文件系統與緩衝區高速緩存中內容的一致性,UNIX系統提供了sync、fsync和fdatasync三個函數爲強制寫入硬盤提供支持。
Redis每次事件輪訓結束前(beforeSleep
)都會調用函數flushAppendOnlyFile
,flushAppendOnlyFile
會把AOF緩衝區(aof_buf
)中的數據寫入內核緩衝區,而且根據appendfsync
配置來決定採用何種策略把內核緩衝區中的數據寫入磁盤,即調用fsync()
。該配置有三個可選項always
、no
、everysec
,具體說明以下:
fsync()
,是安全性最高、性能最差的一種策略。fsync()
。性能最好,安全性最差。fsync()
。這是官方建議的同步策略,也是默認配置,作到兼顧性能和數據安全性,理論上只有在系統忽然宕機的狀況下丟失1秒的數據。注意:上面介紹的策略受配置項no-appendfsync-on-rewrite
的影響,它的做用是告知Redis:AOF文件重寫期間是否禁止調用fsync(),默認是no。
若是appendfsync
設置爲always
或everysec
,後臺正在進行的BGSAVE
或者BGREWRITEAOF
消耗過多的磁盤I/O,在某些Linux系統配置下,Redis對fsync()的調用可能阻塞很長時間。然而這個問題尚未修復,由於即便是在不一樣的線程中執行fsync()
,同步寫入操做也會被阻塞。
爲了緩解此問題,可使用該選項,以防止在進行BGSAVE
或BGREWRITEAOF
時在主進程中調用fsync()。
yes
意味着,若是子進程正在進行BGSAVE
或BGREWRITEAOF
,AOF的持久化能力就與appendfsync
設置爲no
有着相同的效果。最糟糕的狀況下,這可能會致使30秒的緩存數據丟失。yes
,不然保持爲no
。如前面提到的,Redis長時間運行,命令不斷寫入AOF,文件會愈來愈大,不加控制可能影響宿主機的安全。
爲了解決AOF文件體積問題,Redis引入了AOF文件重寫功能,它會根據Redis內數據對象的最新狀態生成新的AOF文件,新舊文件對應的數據狀態一致,可是新文件會具備較小的體積。重寫既減小了AOF文件對磁盤空間的佔用,又能夠提升Redis重啓時數據恢復的速度。仍是下面這個例子,舊文件中的6條命令等同於新文件中的1條命令,壓縮效果顯而易見。
咱們說,AOF文件太大時會觸發AOF文件重寫,那究竟是多大呢?有哪些狀況會觸發重寫操做呢?
**
與RDB方式同樣,AOF文件重寫既能夠手動觸發,也會自動觸發。手動觸發直接調用bgrewriteaof
命令,若是當時無子進程執行會馬上執行,不然安排在子進程結束後執行。自動觸發由Redis的週期性方法serverCron
檢查在知足必定條件時觸發。先了解兩個配置項:
BGREWRITEAOF
時AOF文件佔用空間最小值,默認爲64MB;Redis啓動時把aof_base_size
初始化爲當時aof文件的大小,Redis運行過程當中,當AOF文件重寫操做完成時,會對其進行更新;aof_current_size
爲serverCron
執行時AOF文件的實時大小。當知足如下兩個條件時,AOF文件重寫就會觸發:
增加比例:(aof_current_size - aof_base_size) / aof_base_size > auto-aof-rewrite-percentage 文件大小:aof_current_size > auto-aof-rewrite-min-size
手動觸發與自動觸發的代碼以下,一樣在週期性方法serverCron
中:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { /* 省略其餘邏輯 */ /* 若是用戶請求進行AOF文件重寫時,Redis正在執行RDB持久化,Redis會安排在RDB持久化完成後執行AOF文件重寫, * 若是aof_rewrite_scheduled爲true,說明須要執行用戶的請求 */ 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開啓、無子進程運行、增加百分比已設置、當前文件大小超過閾值 */ 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方式,這一策略由配置參數aof-use-rdb-preamble
(使用RDB做爲AOF文件的前半段)控制,默認關閉(no),設置爲yes可開啓。因此,在AOF重寫過程當中文件的寫入會有兩種不一樣的方式。當aof-use-rdb-preamble
的值是:
結合源碼(6.0版本,源碼太多這裏不貼出,可參考aof.c
)及參考資料,繪製AOF重寫(BGREWRITEAOF)流程圖:
結合上圖,總結一下AOF文件重寫的流程:
父進程:
子進程:
aof-use-rdb-preamble
配置,以RDB或AOF方式寫入前半部分,並同步至硬盤;Redis啓動後經過loadDataFromDisk
函數執行數據加載工做。這裏須要注意,雖然持久化方式能夠選擇AOF、RDB或者二者兼用,可是數據加載時必須作出選擇,兩種方式各自加載一遍就亂套了。
理論上,AOF持久化比RDB具備更好的實時性,當開啓了AOF持久化方式,Redis在數據加載時優先考慮AOF方式。並且,Redis 4.0版本後AOF支持了混合持久化,加載AOF文件須要考慮版本兼容性。Redis數據加載流程以下圖所示:
在AOF方式下,開啓混合持久化機制生成的文件是「RDB頭+AOF尾」,未開啓時生成的文件所有爲AOF格式。考慮兩種文件格式的兼容性,若是Redis發現AOF文件爲RDB頭,會使用RDB數據加載的方法讀取並恢復前半部分;而後再使用AOF方式讀取並恢復後半部分。因爲AOF格式存儲的數據爲RESP協議命令,Redis採用僞客戶端執行命令的方式來恢復數據。
若是在AOF命令追加過程當中發生宕機,因爲延遲寫的技術特色,AOF的RESP命令可能不完整(被截斷)。遇到這種狀況時,Redis會按照配置項aof-load-truncated
執行不一樣的處理策略。這個配置是告訴Redis啓動時讀取aof文件,若是發現文件被截斷(不完整)時該如何處理:
Redis提供了兩種持久化的選擇:RDB支持以特定的實踐間隔爲數據集生成時間點快照;AOF把Redis Server收到的每條寫指令持久化到日誌中,待Redis重啓時經過重放命令恢復數據。日誌格式爲RESP協議,對日誌文件只作append操做,無損壞風險。而且當AOF文件過大時能夠自動重寫壓縮文件。
固然,若是你不須要對數據進行持久化,也能夠禁用Redis的持久化功能,可是大多數狀況並不是如此。實際上,咱們時有可能同時使用RDB和AOF兩種方式的,最重要的就是咱們要理解二者的區別,以便合理使用。
redis-check-aof
工具修復;對RDB和AOF兩種持久化方式的工做原理、執行流程及優缺點了解後,咱們來思考下,實際場景中應該怎麼權衡利弊,合理的使用兩種持久化方式。若是僅僅是使用Redis做爲緩存工具,全部數據能夠根據持久化數據庫進行重建,則可關閉持久化功能,作好預熱、緩存穿透、擊穿、雪崩之類的防禦工做便可。
通常狀況下,Redis會承擔更多的工做,如分佈式鎖、排行榜、註冊中心等,持久化功能在災難恢復、數據遷移方面將發揮較大的做用。建議遵循幾個原則:
經過上面的分析,咱們都知道RDB的快照、AOF的重寫都須要fork,這是一個重量級操做,會對Redis形成阻塞。所以爲了避免影響Redis主進程響應,咱們須要儘量下降阻塞。