共計 14395 個字符,預計需要花費 36 分鐘才能閱讀完成。
這篇文章將為大家詳細講解有關 Redis 中持久化原理是什么,丸趣 TV 小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。
Redis 持久化機制:
## 寫在前面
本文從整體上詳細介紹 Redis 的兩種持久化方式,包含工作原理、持久化流程及實踐策略,以及背后的一些理論知識。上一篇文章僅介紹了 RDB 持久化,但是 Redis 持久化是一個整體,單獨介紹不成體系,故重新整理。【相關推薦:Redis 視頻教程】
Redis 是一個內存數據庫,所有的數據將保存在內存中,這與傳統的 MySQL、Oracle、SqlServer 等關系型數據庫直接把數據保存到硬盤相比,Redis 的讀寫效率非常高。但是保存在內存中也有一個很大的缺陷,一旦斷電或者宕機,內存數據庫中的內容將會全部丟失。為了彌補這一缺陷,Redis 提供了把內存數據持久化到硬盤文件,以及通過備份文件來恢復數據的功能,即 Redis 持久化機制。
Redis 支持兩種方式的持久化:RDB 快照和 AOF。
RDB 持久化
RDB 快照用官方的話來說:RDB 持久化方案是按照指定時間間隔對你的數據集生成的時間點快照(point-to-time snapshot)。它以緊縮的二進制文件保存 Redis 數據庫某一時刻所有數據對象的內存快照,可用于 Redis 的數據備份、轉移與恢復。到目前為止,仍是官方的默認支持方案。
RDB 工作原理
既然說 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 中),先簡單說下兩者差別:
rdbSave:是同步執行的,方法調用后就會立刻啟動持久化流程。由于 Redis 是單線程模型,持久化過程中會阻塞,Redis 無法對外提供服務;
rdbSaveBackground:是后臺(異步)執行的,該方法會 fork 出子進程,真正的持久化過程是在子進程中執行的(調用 rdbSave),主進程會繼續提供服務;
RDB 持久化的觸發必然離不開以上兩個方法,觸發的方式分為手動和自動。手動觸發容易理解,是指我們通過 Redis 客戶端人為的對 Redis 服務端發起持久化備份指令,然后 Redis 服務端開始執行持久化流程,這里的指令有 save 和 bgsave。自動觸發是 Redis 根據自身運行要求,在滿足預設條件時自動觸發的持久化流程,自動觸發的場景有如下幾個(摘自這篇文章):
serverCron 中 save m n 配置規則自動觸發;
從節點全量復制時,主節點發送 rdb 文件給從節點完成復制操作,主節點會出發 bgsave;
執行 debug reload 命令重新加載 redis 時;
默認情況下(未開啟 AOF)執行 shutdown 命令時,自動執行 bgsave;
結合源碼及參考文章,我整理了 RDB 持久化流程來幫助大家有個整體的了解,然后再從一些細節進行說明。
從上圖可以知道:
自動觸發的 RDB 持久化是通過 rdbSaveBackground 以子進程方式執行的持久化策略;
手動觸發是以客戶端命令方式觸發的,包含 save 和 bgsave 兩個命令,其中 save 命令是在 Redis 的命令處理線程以阻塞的方式調用 rdbSave 方法完成的。
自動觸發流程是一個完整的鏈路,涵蓋了 rdbSaveBackground、rdbSave 等,接下來我以 serverCron 為例分析一下整個流程。
save 規則及檢查
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
rdbSaveBackground 是 RDB 持久化的輔助性方法,主要工作是 fork 子進程,然后根據調用方(父進程或者子進程)不同,有兩種不同的執行邏輯。
如果調用方是父進程,則 fork 出子進程,保存子進程信息后直接返回。
如果調用方是子進程則調用 rdbSave 執行 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);
// 執行 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 以操作的耗時。
rdbSave
Redis 的 rdbSave 函數是真正進行 RDB 持久化的函數,流程、細節賊多,整體流程可以總結為:創建并打開臨時文件、Redis 內存數據寫入臨時文件、臨時文件寫入磁盤、臨時文件重命名為正式 RDB 文件、更新持久化狀態信息(dirty、lastsave)。其中“Redis 內存數據寫入臨時文件”最為核心和復雜,寫入過程直接體現了 RDB 文件的文件格式,本著一圖勝千言的理念,我按照源碼流程繪制了下圖。
補充說明一下,上圖右下角“遍歷當前數據庫的鍵值對并寫入”這個環節會根據不同類型的 Redis 數據類型及底層數據結構采用不同的格式寫入到 RDB 文件中,不再展開了。我覺得大家對整個過程有個直觀的理解就好,這對于我們理解 Redis 內部的運作機制大有裨益。
AOF 持久化
上一節我們知道 RDB 是一種時間點(point-to-time)快照,適合數據備份及災難恢復,由于工作原理的“先天性缺陷”無法保證實時性持久化,這對于緩存丟失零容忍的系統來說是個硬傷,于是就有了 AOF。
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 所要考慮的工作要比這個例子復雜的多:
Redis Server 啟動后,aof 文件一直在追加命令,文件會越來越大。文件越大,Redis 重啟后恢復耗時越久;文件太大,轉移工作就越難;不加管理,可能撐爆硬盤。很顯然,需要在合適的時機對文件進行精簡。例子中的 5 條 incr 指令很明顯的可以替換為為一條 set 命令,存在很大的壓縮空間。
眾所周知,文件 I / O 是操作系統性能的短板,為了提高效率,文件系統設計了一套復雜的緩存機制,Redis 操作命令的追加操作只是把數據寫入了緩沖區(aof_buf),從緩沖區到寫入物理文件在性能與安全之間權衡會有不同的選擇。
文件壓縮即意味著重寫,重寫時即可依據已有的 aof 文件做命令整合,也可以先根據當前 Redis 內數據的狀態做快照,再把存儲快照過程中的新增的命令做追加。
aof 備份后的文件是為了恢復數據,結合 aof 文件的格式、完整性等因素,Redis 也要設計一套完整的方案做支持。
持久化流程
從流程上來看,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,具體說明如下:
always:每次都調用 fsync(),是安全性最高、性能最差的一種策略。
no:不會調用 fsync()。性能最好,安全性最差。
everysec:僅在滿足同步條件時調用 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 檢查在滿足一定條件時觸發。先了解兩個配置項:
auto-aof-rewrite-percentage:代表當前 AOF 文件大小(aof_current_size)和上一次重寫后 AOF 文件大小(aof_base_size)相比,增長的比例。
auto-aof-rewrite-min-size:表示運行 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 的值是:
no:按照 AOF 格式寫入命令,與 4.0 前版本無差別;
yes:先按照 RDB 格式寫入數據狀態,然后把重寫期間 AOF 緩沖區的內容以 AOF 格式寫入,文件前半部分為 RDB 格式,后半部分為 AOF 格式。
結合源碼(6.0 版本,源碼太多這里不貼出,可參考 aof.c)及參考資料,繪制 AOF 重寫(BGREWRITEAOF)流程圖:
結合上圖,總結一下 AOF 文件重寫的流程:
rewriteAppendOnlyFileBackground 開始執行,檢查是否有正在進行的 AOF 重寫或 RDB 持久化子進程:如果有,則退出該流程;如果沒有,則繼續創建接下來父子進程間數據傳輸的通信管道。執行 fork()操作,成功后父子進程分別執行不同的流程。
父進程:
記錄子進程信息(pid)、時間戳等;
繼續響應其他客戶端請求;
收集 AOF 重寫期間的命令,追加至 aof_rewrite_buffer;
等待并向子進程同步 aof_rewrite_buffer 的內容;
子進程:
修改當前進程名稱,創建重寫所需的臨時文件,調用 rewriteAppendOnlyFile 函數;
根據 aof-use-rdb-preamble 配置,以 RDB 或 AOF 方式寫入前半部分,并同步至硬盤;
從父進程接收增量 AOF 命令,以 AOF 方式寫入后半部分,并同步至硬盤;
重命名 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 文件,如果發現文件被截斷(不完整)時該如何處理:
yes:則盡可能多的加載數據,并以日志的方式通知用戶;
no:則以系統錯誤的方式崩潰,并禁止啟動,需要用戶修復文件后再重啟。
總結
Redis 提供了兩種持久化的選擇:RDB 支持以特定的實踐間隔為數據集生成時間點快照;AOF 把 Redis Server 收到的每條寫指令持久化到日志中,待 Redis 重啟時通過重放命令恢復數據。日志格式為 RESP 協議,對日志文件只做 append 操作,無損壞風險。并且當 AOF 文件過大時可以自動重寫壓縮文件。
當然,如果你不需要對數據進行持久化,也可以禁用 Redis 的持久化功能,但是大多數情況并非如此。實際上,我們時有可能同時使用 RDB 和 AOF 兩種方式的,最重要的就是我們要理解兩者的區別,以便合理使用。
RDB vs AOFRDB 優點
RDB 是一個緊湊壓縮的二進制文件,代表 Redis 在某一個時間點上的數據快照,非常適合用于備份、全量復制等場景。
RDB 對災難恢復、數據遷移非常友好,RDB 文件可以轉移至任何需要的地方并重新加載。
RDB 是 Redis 數據的內存快照,數據恢復速度較快,相比于 AOF 的命令重放有著更高的性能。
RDB 缺點
RDB 方式無法做到實時或秒級持久化。因為持久化過程是通過 fork 子進程后由子進程完成的,子進程的內存只是在 fork 操作那一時刻父進程的數據快照,而 fork 操作后父進程持續對外服務,內部數據時刻變更,子進程的數據不再更新,兩者始終存在差異,所以無法做到實時性。
RDB 持久化過程中的 fork 操作,會導致內存占用加倍,而且父進程數據越多,fork 過程越長。
Redis 請求高并發可能會頻繁命中 save 規則,導致 fork 操作及持久化備份的頻率不可控;
RDB 文件有文件格式要求,不同版本的 Redis 會對文件格式進行調整,存在老版本無法兼容新版本的問題。
AOF 優點
AOF 持久化有更好的實時性,我們可以選擇三種不同的方式(appendfsync):no、every second、always,every second 作為默認的策略具有最好的性能,極端情況下可能會丟失一秒的數據。
AOF 文件只有 append 操作,無復雜的 seek 等文件操作,沒有損壞風險。即使最后寫入數據被截斷,也很容易使用 redis-check-aof 工具修復;
當 AOF 文件變大時,Redis 可在后臺自動重寫。重寫過程中舊文件會持續寫入,重寫完成后新文件將變得更小,并且重寫過程中的增量命令也會 append 到新文件。
AOF 文件以已于理解與解析的方式包含了對 Redis 中數據的所有操作命令。即使不小心錯誤的清除了所有數據,只要沒有對 AOF 文件重寫,我們就可以通過移除最后一條命令找回所有數據。
AOF 已經支持混合持久化,文件大小可以有效控制,并提高了數據加載時的效率。
AOF 缺點
對于相同的數據集合,AOF 文件通常會比 RDB 文件大;
在特定的 fsync 策略下,AOF 會比 RDB 略慢。一般來講,fsync_every_second 的性能仍然很高,fsync_no 的性能與 RDB 相當。但是在巨大的寫壓力下,RDB 更能提供最大的低延時保障。
在 AOF 上,Redis 曾經遇到一些幾乎不可能在 RDB 上遇到的罕見 bug。一些特殊的指令(如 BRPOPLPUSH)導致重新加載的數據與持久化之前不一致,Redis 官方曾經在相同的條件下進行測試,但是無法復現問題。
使用建議
對 RDB 和 AOF 兩種持久化方式的工作原理、執行流程及優缺點了解后,我們來思考下,實際場景中應該怎么權衡利弊,合理的使用兩種持久化方式。如果僅僅是使用 Redis 作為緩存工具,所有數據可以根據持久化數據庫進行重建,則可關閉持久化功能,做好預熱、緩存穿透、擊穿、雪崩之類的防護工作即可。
一般情況下,Redis 會承擔更多的工作,如分布式鎖、排行榜、注冊中心等,持久化功能在災難恢復、數據遷移方面將發揮較大的作用。建議遵循幾個原則:
不要把 Redis 作為數據庫,所有數據盡可能可由應用服務自動重建。
使用 4.0 以上版本 Redis,使用 AOF+RDB 混合持久化功能。
合理規劃 Redis 最大占用內存,防止 AOF 重寫或 save 過程中資源不足。
避免單機部署多實例。
生產環境多為集群化部署,可在 slave 開啟持久化能力,讓 master 更好的對外提供寫服務。
備份文件應自動上傳至異地機房或云存儲,做好災難備份。
關于 fork()
通過上面的分析,我們都知道 RDB 的快照、AOF 的重寫都需要 fork,這是一個重量級操作,會對 Redis 造成阻塞。因此為了不影響 Redis 主進程響應,我們需要盡可能降低阻塞。
降低 fork 的頻率,比如可以手動來觸發 RDB 生成快照、與 AOF 重寫;
控制 Redis 最大使用內存,防止 fork 耗時過長;
使用更高性能的硬件;
合理配置 Linux 的內存分配策略,避免因為物理內存不足導致 fork 失敗。
關于“Redis 中持久化原理是什么”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,使各位可以學到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。