京東二面:Redis持久化方式有哪些?說下各自的優(yōu)缺點?線上環(huán)境如何進行配置的?
引言
Redis作為一款高性能的鍵值存儲系統(tǒng),廣泛應用于緩存、消息隊列、會話存儲等多種場景,成為提升應用性能的關鍵組件。作為內存數(shù)據(jù)庫,它存儲在內存中的數(shù)據(jù)在服務器重啟或發(fā)生意外崩潰時將會丟失。所以需要持久化機制能夠將內存中的數(shù)據(jù)保存到磁盤上,從而在Redis服務停止或重啟后能夠恢復數(shù)據(jù),保證數(shù)據(jù)的安全性。
在實際開發(fā)中的一些通用方案是業(yè)務數(shù)據(jù)最終都保存在DB中,所以也可以從數(shù)據(jù)庫中恢復Redis數(shù)據(jù)。但是這種方式,會造成數(shù)據(jù)庫的性能瓶頸,特別是在大量Redis數(shù)據(jù)需要恢復的時候。并且Redis中緩存的數(shù)據(jù),會經過一些特殊的處理,跟DB中表數(shù)據(jù)可能會不一致,所以從DB中恢復Redis數(shù)據(jù)還需要一些特殊的業(yè)務梳理。
所以就需要從Redis自身出發(fā)會解決這個問題,Redis設計了兩種主要的持久化機制:RDB和AOF。實際上,對于持久化方案,官方文檔中給出了三種方式:RDB,AOF,RDB+AOF。
圖片
關于Redis持久化機制的官方文檔:Redis persistence | Docs
Redis持久化方式
Redis提供了兩種核心的持久化策略:RDB(快照)和AOF(僅追加日志),隨著Redis版本的演進,還引入了混合持久化的方式,進一步優(yōu)化了Redis的持久化。
RDB
RDB,即Redis Database,是一種快照式持久化方式,它通過定期創(chuàng)建數(shù)據(jù)集的快照來實現(xiàn)數(shù)據(jù)的持久存儲。在預設條件(如時間間隔、鍵值變化次數(shù))滿足時,Redis會生成當前內存中所有數(shù)據(jù)的備份文件。當Redis服務需要重新啟動時,可以直接加載這些RDB文件,從而快速恢復數(shù)據(jù)。由于RDB文件是二進制格式,加載速度通常較快。生成快照有自動觸發(fā)和手動觸發(fā)兩種方式。
自動觸發(fā)
Redis會將數(shù)據(jù)集的快照保存到名為dump.rdb的二進制文件中,保存到磁盤上。我們可以通過Redis配置(redis.conf文件),使其在數(shù)據(jù)集中至少有M個(changes)更改時,每N秒(seconds)自動保存一次數(shù)據(jù)集。
save <seconds> <changes> [<seconds> <changes> ...]
# 生成的rdb文件名稱
dbfilename dump.rdb
# rdb文件保存路徑,可自定義
dir ./
# rdb快照保存配置
save 3600 1 300 100 60 10000
# 如果至少進行了1次更改,那么在3600秒(1小時)后保存;
# 如果至少進行了100次更改,那么在300秒(5分鐘)后保存;
# 如果至少進行了10000次更改,那么在60秒后保存。
# 關閉RDB快照
save ""
當Redis服務正常關閉時,如果沒有啟用AOF(Append Only File)持久化,它也會執(zhí)行一次RDB快照,確保數(shù)據(jù)被保存到磁盤上。
手動觸發(fā)
我們還可以通過調用SAVE或BGSAVE命令來手動觸發(fā)快照保存。這兩個命令的區(qū)別在于執(zhí)行數(shù)據(jù)持久化操作時是否阻塞主線程。
SAVE命令
當執(zhí)行SAVE命令時,Redis會直接調用rdbSave函數(shù)來創(chuàng)建數(shù)據(jù)快照,并將當前數(shù)據(jù)集寫入到磁盤上的RDB文件中。這是一個同步操作,在保存過程期間,Redis的主進程會被阻塞,無法處理任何其他客戶端的請求,直到整個保存操作完成。所以,如果數(shù)據(jù)集較大,此操作可能會導致Redis服務在較長時間內無法響應,影響服務的可用性。
BGSAVE命令
使用BGSAVE命令時,Redis會通過操作系統(tǒng)fork()調用創(chuàng)建一個子進程,由這個子進程負責執(zhí)行rdbSave函數(shù)和數(shù)據(jù)持久化工作,即創(chuàng)建數(shù)據(jù)快照并寫入RDB文件。持久化操作在子進程中進行,Redis的主進程(父進程)可以繼續(xù)處理來自客戶端的請求,不會因為持久化操作而阻塞。當子進程完成快照創(chuàng)建后,會向主進程發(fā)送一個信號,通知任務完成,然后子進程關閉。
BGSAVE命令執(zhí)行流程
具體執(zhí)行流程如下:
1. 當Redis客戶端發(fā)出BGSAVE命令,或者根據(jù)配置規(guī)則自動觸發(fā)BGSAVE操作時,持久化進程開始
2. Redis主進程檢查是否有正在進行的子進程正在執(zhí)行BGSAVE或AOF rewrite操作。如果存在這種操作,為了避免資源競爭和數(shù)據(jù)一致性問題,主進程不會再次發(fā)起B(yǎng)GSAVE,可能直接返回一個錯誤或忽略此次請求。
3. 如果沒有其他持久化操作正在進行,主進程會執(zhí)行fork()系統(tǒng)調用來創(chuàng)建一個子進程。fork()調用在執(zhí)行時會暫時阻塞主進程,直到子進程完全創(chuàng)建。這個過程中,Redis的內存數(shù)據(jù)不會被復制,而是通過操作系統(tǒng)實現(xiàn)的寫時復制(Copy-On-Write, COW)機制共享給子進程。
4. 子進程開始工作,它將內存中的數(shù)據(jù)逐步寫入到一個新的臨時RDB文件中。這個過程對主進程是透明的,不會影響主進程處理客戶端的讀寫請求。
5. 當所有數(shù)據(jù)寫入完畢后,子進程會使用原子操作(通常借助操作系統(tǒng)提供的rename函數(shù))將臨時RDB文件替換為正式的RDB文件。保證RBD文件的完整性。
6. 完成上述步驟后,子進程會向主進程發(fā)送一個信號,告知BGSAVE操作已完成。
RDB優(yōu)缺點
優(yōu)點
? 快速恢復:RDB文件加載速度非???,因為它直接將數(shù)據(jù)載入內存而無需逐條執(zhí)行命令,適合用于快速重啟服務后的數(shù)據(jù)恢復。
? 資源占用低:RDB文件(二進制文件)通常較小,占用磁盤空間少,備份和傳輸更加高效。
缺點
? 數(shù)據(jù)丟失風險:如果Redis在兩次快照之間崩潰,那么從上一次快照之后的所有數(shù)據(jù)變更將會丟失。因此,RDB更適合對數(shù)據(jù)實時性要求不高,但重視恢復速度的場景。
? 無法進行部分恢復:一旦發(fā)生故障,只能整體恢復到最近一次快照的狀態(tài),無法選擇性恢復特定數(shù)據(jù)。
幾個常見的面試問題
? 如果我們的Redis內存的數(shù)據(jù)比較大(可能給Redis分配的內存較大),我們生成快照保存到磁盤,就會花費的時間要久一些,如果此時Redis的主進程依然還要在處理一些寫數(shù)據(jù)的請求,那么我們如何保證數(shù)據(jù)一致性呢?
有上述RDB的執(zhí)行流程中可以看出,主進程通過寫時復制,將數(shù)據(jù)共享給子進程。在持久化操作開始時,父子進程共享相同的內存空間,直到任一進程嘗試修改數(shù)據(jù)。當主進程接收到寫操作請求并需要修改數(shù)據(jù)時,操作系統(tǒng)會為被修改的數(shù)據(jù)分配新的內存空間,保持原數(shù)據(jù)不變供子進程快照使用。即便在快照生成期間有寫操作,也能保證快照數(shù)據(jù)反映的是快照操作開始那一刻的數(shù)據(jù)狀態(tài),確保了數(shù)據(jù)的一致性。
圖片
? 在生成快照的時候,Redis服務跪了,數(shù)據(jù)會不會丟失? 在上述BGSAVE命令執(zhí)行流程時,在最后的寫入磁盤時,子進程會使用原子操作將臨時RDB文件替換為正式的RDB文件,保證RBD文件的完整性。也就是說Redis在執(zhí)行BGSAVE命令生成快照時,會先將數(shù)據(jù)寫入到一個臨時的RDB文件中,而不是直接覆蓋原有的RDB文件。只有當新的快照完全且成功地寫入磁盤之后,才會用這個新的快照文件替換掉舊的RDB文件。這一過程是原子性的,確保了要么新快照完全成功,要么不會有任何改動,從而避免了部分寫入導致的文件損壞,避免數(shù)據(jù)的丟失。
? 快照的生成時間間隔是不是越小越好?如何合理的設置? 對于RDB生成快照來做數(shù)據(jù)持久化,就是通過連續(xù)的生成數(shù)據(jù)快照將數(shù)據(jù)保存到磁盤。一單服務發(fā)生故障,那么我們就可以通過磁盤上的文件恢復數(shù)據(jù)。那么,為了防止數(shù)據(jù)丟失,或者數(shù)據(jù)丟失過多時,我們可以通過減少時間間隔來實現(xiàn),也就是說,最好可以一直不停的生成快照,這樣就可以達到數(shù)據(jù)不丟失的目的。
圖片
如上圖,T0時刻生成了一次快照,那么在t時刻時發(fā)生了一次數(shù)據(jù)修改寫入,在寫入后到生成下一次快照之前,發(fā)生了故障,那么我們恢復數(shù)據(jù)時,因為T0+t時刻的快照還沒生成,數(shù)據(jù)恢復時只能使用T0時刻的快照,那么在t時刻發(fā)生的數(shù)據(jù)寫入就會丟失。所以t時刻的間隔如果變的更小,就會多生成快照,就有很大的幾率減少數(shù)據(jù)丟失的。
那么是不是說這個生成快照的間隔越小越好,甚至說1秒生成一次快照?因為從BGSAVE的執(zhí)行流程上看,它是通過fork一個子進程去實現(xiàn)快照的生成,并不會阻塞主進程的數(shù)據(jù)寫入。
事實上,這種說法是錯誤的。理論上,縮短快照間隔可以減少數(shù)據(jù)丟失的風險,因為一旦系統(tǒng)崩潰,可以恢復到更接近崩潰時間點的數(shù)據(jù)狀態(tài)。但是,過于緊密的快照間隔會引發(fā)大量的磁盤I/O操作,可能導致數(shù)據(jù)寫操作堆積,反而影響數(shù)據(jù)的一致性。
并且,雖然Redis使用BGSAVE命令在子進程中生成快照,避免了主線程的長時間阻塞,但是fork子進程的操作本身是阻塞的,并且內存越大,fork操作所需的時間越長。頻繁的fork會導致服務在短時間內頻繁阻塞,影響響應速度。
那么,如果我們可以只對增量的數(shù)據(jù)進行快照保存,在第一次進行全量的快照保存后,后續(xù)的快照都只對增量的數(shù)據(jù)(其實這個比較很好理解,這種方案我們也很常見,比如我們在對接一些第三方系統(tǒng)時,在拉取數(shù)據(jù)時,我們在第一次全量拉去后,后面的數(shù)據(jù)都只會拉去變化的數(shù)據(jù))。那么對于增量的數(shù)據(jù),首先我們要知道那些數(shù)據(jù)變化了,此時,就需要Redis的另外一種機制:AOF了。
AOF
AOF(Append Only File)持久化是一種以日志形式記錄每次寫操作的方式,以此來保證數(shù)據(jù)的持久性。與RDB不同,它提供了更高的數(shù)據(jù)完整性保障,因為其記錄了從Redis啟動以來的所有寫操作,理論上可以做到數(shù)據(jù)零丟失。
每當有寫命令(如SET、HSET等)被執(zhí)行時,Redis就會將該命令以明文形式追加到AOF文件的末尾。這意味著AOF文件隨著時間推移會不斷增長,記錄著Redis服務器上的所有數(shù)據(jù)修改歷史。
并且AOF采用的是一種"寫后日志"機制,即Redis在處理寫命令時,先執(zhí)行寫操作,將數(shù)據(jù)變更寫入內存,然后才將該操作命令追加到AOF日志文件中。
寫后日志這種機制與一些數(shù)據(jù)庫系統(tǒng)采用的“預寫日志”策略恰好相反,后者是在實際數(shù)據(jù)寫入前先記錄日志。
為什么采用寫后日志?
1. 性能考慮:通過先執(zhí)行寫命令,Redis可以更快地響應客戶端,因為寫入內存的速度遠快于寫入磁盤。這樣可以減少客戶端等待時間。
2. 簡化實現(xiàn):寫后日志避免了在記錄日志之前對命令進行語法檢查的需要。如果先記錄日志再執(zhí)行命令,錯誤的命令也可能被記錄,導致在使用日志恢復數(shù)據(jù)時出現(xiàn)問題。而在Redis中,因為命令已經執(zhí)行成功,所以寫入日志的命令都是已驗證過的,可以安全重放。
3. 簡化恢復邏輯:在Redis重啟時,通過重放AOF日志中的命令來恢復數(shù)據(jù)狀態(tài),由于這些命令都是曾經成功執(zhí)行過的,理論上可以無誤地重建內存數(shù)據(jù)結構,降低了數(shù)據(jù)恢復的復雜度。
當然這中機制也存在著一些潛在風險,比如數(shù)據(jù)丟失風險,如果在命令執(zhí)行后、但未記錄到AOF日志前發(fā)生故障,這部分數(shù)據(jù)更新將丟失。
AOF實現(xiàn)流程
1. 命令追加(Append): 當Redis服務器配置開啟了AOF持久化功能后,每當執(zhí)行一個寫命令(如SET、HSET、LPUSH等),服務器并不會立即寫入到磁盤,而是先將這條命令以Redis協(xié)議的格式追加到內存中的一個緩沖區(qū),即aof_buf。
這樣做是為了減少直接磁盤I/O的頻率,提高寫入命令的處理速度。
2. 文件寫入(Write): 緩沖區(qū)中的內容并不會一直停留,而是會按照一定的策略(由配置項appendfsync控制)被寫入到AOF文件中。AOF提供了三種同步策略來控制命令寫入AOF文件:
? always:每個寫命令都會立即同步到磁盤,最安全但性能消耗最大。
? everysec(默認):每秒執(zhí)行一次fsync操作,平衡了數(shù)據(jù)安全性與性能。最多丟失一秒的數(shù)據(jù)。
? no:完全依賴于操作系統(tǒng)來決定何時將數(shù)據(jù)寫入磁盤,性能最佳但可能丟失一秒內的數(shù)據(jù)。
3. 文件同步(Sync): 文件同步操作是將內存中已寫入AOF文件的數(shù)據(jù)真正持久化到磁盤上。上述寫入AOF文件策略的不同,同步操作的時機也會有所不同。文件同步系統(tǒng)調用是同步操作的核心,它確保了緩沖區(qū)中的數(shù)據(jù)被物理寫入磁盤,從而在硬件故障時也能保證數(shù)據(jù)不丟失。
AOF重寫機制
隨著時間的推移,AOF文件會因為不斷追加寫命令而逐漸增大,這不僅占用大量磁盤空間,還會影響數(shù)據(jù)恢復的速度。AOF重寫機制通過生成一個更緊湊的新AOF文件來解決這個問題,新文件僅包含重建當前數(shù)據(jù)集狀態(tài)所需的最小命令集合,從而替代原有的龐大AOF文件。
AOF重寫可以通過手動執(zhí)行BGREWRITEAOF命令來觸發(fā),也可以通過配置自動觸發(fā)。自動觸發(fā)的條件通常是AOF文件大小增長到一定比例(如通過auto-aof-rewrite-percentage配置)或達到一定的最小尺寸(如通過auto-aof-rewrite-min-size配置)。
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
類似RDB的BGSAVE命令的工作機制,當重寫命令發(fā)出后,Redis會通過fork()系統(tǒng)調用創(chuàng)建一個子進程。子進程開始遍歷Redis內存中的數(shù)據(jù)結構,對每個鍵值對執(zhí)行相應最少的寫命令(如SET、HMSET等),將這些命令寫入到一個新的臨時AOF文件中。這個過程中,子進程不會處理任何網絡請求,也不會影響主進程處理客戶端命令的能力。
與RDB不同的是,在子進程重寫期間,主進程會繼續(xù)接收和處理客戶端的寫請求。為了避免這些新命令丟失,主進程會將它們追加到一個內存緩沖區(qū)(AOF重寫緩沖區(qū))中。
一旦子進程完成重寫,會將臨時AOF文件替換掉舊的AOF文件。(為了保證數(shù)據(jù)一致性,這一過程通常需要原子操作。)在新AOF文件替換舊文件后,主進程會將之前累積在AOF重寫緩沖區(qū)中的命令追加到新AOF文件的末尾,確保所有數(shù)據(jù)變更都得到記錄。
AOF重寫流程
如何配置AOF
默認情況下,Redis是沒有開啟AOF的,可以通過配置redis.conf文件來開啟AOF持久化。
# 開啟AOF,默認為no,關閉
appendonly yes
# AOF文件名
appendfilename "appendonly.aof"
# 控制AOF fsync策略
appendfsync everysec
# 文件重寫策略
aof-rewrite-incremental-fsync yes
# AOF重寫觸發(fā)條件,當AOF文件增長到上一次重寫后大小的百分比時觸發(fā)
auto-aof-rewrite-percentage 100
# 觸發(fā)重寫的最小AOF文件大小
auto-aof-rewrite-min-size 64mb
# 在Redis啟動時如何加載AOF文件,默認為yes
aof-load-truncated yes
# aof重寫期間是否同步
no-appendfsync-on-rewrite no
AOF的優(yōu)缺點
優(yōu)點
? 幾乎可以實現(xiàn)數(shù)據(jù)的零丟失,即使在服務器崩潰后也能恢復到最新狀態(tài)。
? 通過重寫機制可以有效控制文件大小,且在極端情況下,即使AOF文件損壞,也往往能通過跳過錯誤命令來恢復大部分數(shù)據(jù)。
缺點
? AOF文件會不斷膨脹,占用大量磁盤空間。
? AOF在服務器重啟時需要逐條執(zhí)行命令來恢復數(shù)據(jù),這會消耗更多時間。
幾個常見的面試題
? AOF重寫會阻塞主進程嗎? 由上述AOF重寫流程中,主進程執(zhí)行AOF重寫命令時,類似RDB的BGSAVE命令,也會fork出來一個子進程用于AOF的重寫,此時并不會阻塞主進程的讀寫操作。當然,在fork子進程時會阻塞。
? 當AOF重寫時,此時的寫操作的數(shù)據(jù)會不會丟失? 由AOF重寫流程,在子進程執(zhí)行重寫AOF時,如果此時有數(shù)據(jù)寫操作,此時仍然由主進程完成,同時主進程再把數(shù)據(jù)寫入內存后,會把命令寫到一個內存緩沖區(qū),也就是AOF重寫緩沖區(qū),在子進程完成AOF重寫,替換舊的AOF文件,生成新的AOF文件之后,會通知主進程,此時主進程會講重寫緩沖區(qū)的命令追加到新的AOF文件之后。這樣就保證數(shù)據(jù)的完整性。
? 為什么AOF重寫不復用原AOF日志? AOF重寫不直接復用原AOF日志的原因主要是為了確保數(shù)據(jù)的一致性和提高重寫效率。AOF重寫的目的在于生成一個盡可能小的新AOF文件,該文件僅包含重建當前數(shù)據(jù)集狀態(tài)所需的最少命令集。直接編輯原AOF文件難以直接去除冗余命令(比如某條數(shù)據(jù)可能有數(shù)次的變更后產生,其以前的命令就意義不大了)。從AOF重寫流程上看,Redis會遍歷內存數(shù)據(jù),將可以實現(xiàn)數(shù)據(jù)的最簡潔的命令寫入到臨時的AOF文件。
Redis在執(zhí)行AOF重寫時,主進程仍繼續(xù)處理客戶端命令。如果直接修改原AOF文件,就需要復雜的并發(fā)控制來防止數(shù)據(jù)混亂,而使用新文件重寫可以避免這種復雜性。通過子進程執(zhí)行重寫,與主進程的命令處理互不影響。
如果在嘗試修改原AOF文件的過程中遇到錯誤或系統(tǒng)崩潰,可能會導致AOF文件損壞,進而影響數(shù)據(jù)恢復。使用獨立的新文件進行重寫,即便過程中出現(xiàn)問題,原AOF文件依然可用。
并且,原AOF文件可能非常大,直接在原文件上進行編輯操作可能會非常緩慢,影響Redis的服務性能。另外,文件系統(tǒng)的直接編輯操作通常比順序寫入新文件更耗時,尤其是在有大量隨機讀寫的情況下。
RDB與AOF的混合使用
為了同時獲得RDB的快速恢復速度和AOF的高度數(shù)據(jù)完整性,Redis支持將這兩種持久化策略結合起來使用?;旌鲜褂肦DB和AOF,取其二者的優(yōu)點,可以實現(xiàn)數(shù)據(jù)安全性和恢復效率的最佳平衡。
在混合持久化模式下,Redis首先利用RDB文件快速恢復到某個時間點的數(shù)據(jù)庫狀態(tài),然后通過重放AOF日志來補充RDB快照之后的數(shù)據(jù)變更。RDB作為基礎恢復,RDB快照提供了某一時間點的全量數(shù)據(jù)視圖,重啟時可以迅速加載到內存,快速使服務可用。而AOF補充細節(jié),AOF日志用于填補從RDB快照以來的數(shù)據(jù)變動,使得Redis能夠逐步恢復到崩潰或重啟前的最新狀態(tài)。
比如,針對RDB快照設置生成間隔的示例,我們使用AOF記錄寫命令,當某一時刻宕機后,恢復數(shù)據(jù)時,我們可以從AOF中恢復上一次備份到這次宕機時保存的寫操作對應的數(shù)據(jù),當數(shù)據(jù)恢復之后,再使用RDB進行生成快照,然后再清空AOF文件。
圖片
Redis重啟時判斷是否開啟aof,如果開啟了aof,那么就優(yōu)先加載aof文件;
這種方式的優(yōu)勢在于:結合RDB的快速加載能力和AOF的細粒度恢復,達到了數(shù)據(jù)安全性和服務恢復速度的雙重優(yōu)化。根據(jù)業(yè)務對數(shù)據(jù)安全等級和恢復時間的要求,可以靈活調整RDB與AOF的比重和策略。通過合理配置RDB保存頻率和AOF重寫策略,可以有效管理磁盤空間使用,避免不必要的資源浪費。
生產環(huán)境的建議
在生產環(huán)境進行Redis持久化時,我們可以從以下幾個方面考慮:
1. 如果存儲在Redis中的數(shù)據(jù)相對不敏感或能便捷地重新生成,可以選擇暫時關閉持久化功能,以減少資源消耗。這樣即使偶發(fā)數(shù)據(jù)丟失,也能通過其他機制迅速恢復數(shù)據(jù)。比如一些數(shù)據(jù)相對于DB來說,并沒有經過一些特殊處理,可以直接從DB中進行恢復,相當于直接在DB做備份。
2. 在同一臺服務器部署多Redis實例時,要注意協(xié)調各實例的持久化操作(如RDB快照或AOF重寫),避免它們同時進行,以免引發(fā)內存、CPU或I/O資源的競爭,推薦采取串行執(zhí)行的方式,確保系統(tǒng)穩(wěn)定運行。
3. 如果配置了Redis的主從,我們可以指定一個或多個從節(jié)點專門負責數(shù)據(jù)備份處理,這樣主節(jié)點就能專注于處理客戶端請求,提高整體服務的響應速度和可用性。
4. 結合使用RDB和AOF兩種持久化方式,可以實現(xiàn)數(shù)據(jù)恢復的速度與完整性之間的一種最佳平衡。RDB提供快速恢復的能力,而AOF則能更詳盡地記錄每一步操作,兩者互補,確保數(shù)據(jù)完整性。