Redis宕機了,Redis如何避免數據丟失?
當被問到在哪些業務場景下你會使用Redis時,你很可能會回答:“我會將其用作緩存,因為Redis將后端數據庫中的數據存儲在內存中,然后直接從內存中讀取數據,因此響應速度非常快。”沒錯,這確實是Redis的一種常見使用場景,但也存在一個絕對不能忽視的問題:一旦服務器宕機,內存中的數據將全部丟失。
解決這個問題的一個顯而易見的方法是從后端數據庫中恢復這些數據。然而,這種方法存在兩個問題:首先,頻繁訪問數據庫會給數據庫帶來巨大的壓力;其次,這些數據是從較慢的數據庫中讀取出來的,性能肯定不如從Redis中讀取,這會導致使用這些數據的應用程序響應速度變慢。因此,對于Redis來說,實現數據持久化以避免從后端數據庫進行恢復是至關重要的。
目前,Redis實現數據持久化主要依靠兩種機制,即AOF(Append-Only File)日志和RDB快照。在接下來的兩個部分,我們將分別深入探討這兩種機制。首先,讓我們重點關注AOF日志。
AOF 日志是如何實現的?
說到日志,我們比較熟悉的是數據庫的寫前日志(Write Ahead Log, WAL),也就是說,在實際寫數據前,先把修改的數據記到日志文件中,以便故障時進行恢復。不過,AOF 日志正好相反,它是寫后日志,“寫后”的意思是 Redis 是先執行命令,把數據寫入內存,然后才記錄日志,如下圖所示:
Redis AOF操作過程
那 AOF 為什么要先執行命令再記日志呢?要回答這個問題,我們要先知道 AOF 里記錄了什么內容。
傳統數據庫的日志,例如 redo log(重做日志),記錄的是修改后的數據,而 AOF 里記錄的是 Redis 收到的每一條命令,這些命令是以文本形式保存的。
我們以 Redis 收到“set testkey testvalue”命令后記錄的日志為例,看看 AOF 日志的內容。其中,“*3”表示當前命令有三個部分,每部分都是由“$+數字”開頭,后面緊跟著具體的命令、鍵或值。這里,“數字”表示這部分中的命令、鍵或值一共有多少字節。例如,“$3 set”表示這部分有 3 個字節,也就是“set”命令。
Redis AOF日志內容
但是,為了避免額外的檢查開銷,Redis 在向 AOF 里面記錄日志的時候,并不會先去對這些命令進行語法檢查。所以,如果先記日志再執行命令的話,日志中就有可能記錄了錯誤的命令,Redis 在使用日志恢復數據時,就可能會出錯。
而寫后日志這種方式,就是先讓系統執行命令,只有命令能執行成功,才會被記錄到日志中,否則,系統就會直接向客戶端報錯。所以,Redis 使用寫后日志這一方式的一大好處是,可以避免出現記錄錯誤命令的情況。
除此之外,AOF 還有一個好處:它是在命令執行后才記錄日志,所以不會阻塞當前的寫操作。
不過,AOF 也有兩個潛在的風險。
首先,考慮一種情況:如果剛剛執行完一個命令,還來不及將該命令記錄到日志中,Redis服務器突然宕機,那么這個命令以及相關數據可能會丟失。當Redis被用作緩存時,數據可以從后端數據庫重新加載以進行恢復。但是,如果Redis被直接用作數據庫,由于命令尚未被記錄到日志中,因此無法使用日志進行數據恢復。
其次,盡管AOF機制避免了當前命令的寫入日志時的阻塞,但這可能會帶來潛在的阻塞風險。這是因為AOF日志寫入也是在Redis的主線程中進行的。如果在將日志寫入磁盤時,磁盤寫入壓力很大,這將導致寫入操作非常緩慢,從而影響后續操作的執行。
經過仔細分析,你會發現這兩種風險都與AOF寫回磁盤的時機有關。這也就表明,如果我們能夠精確控制命令執行后AOF日志寫回磁盤的時機,那么這兩種風險就能夠得到有效地解決。
三種寫回策略
關于AOF機制的問題,有三種不同的策略,對應于AOF配置項appendfsync的三個可選值。
- Always,同步寫回:在每個寫命令執行完后,它會立即將日志同步寫回到磁盤。
- Everysec,每秒寫回:在每個寫命令執行完后,它僅將日志寫入AOF文件的內存緩沖區,然后每隔一秒才將緩沖區中的內容寫入磁盤。
- No,操作系統控制的寫回:在每個寫命令執行完后,它也只是將日志寫入AOF文件的內存緩沖區,而將寫回磁盤的時機由操作系統控制。
然而,針對避免主線程阻塞和減少數據丟失問題,這三種寫回策略都無法做到完美的平衡。以下是對它們的分析:
- 同步寫回可以基本保證數據不會丟失,但它在每個寫命令之后都需要執行一個相對較慢的落盤操作,這不可避免地會影響主線程的性能。
- 盡管操作系統控制的寫回在寫完緩沖區后可以繼續執行后續命令,但它失去了對落盤時機的控制,只要AOF記錄未寫回磁盤,一旦發生宕機,對應的數據就會丟失。
- 每秒寫回采用了一秒寫回一次的頻率,避免了與“同步寫回”相關的性能開銷,但如果發生宕機,上一秒內未寫回磁盤的命令操作仍然會丟失。因此,這個策略可以視為在避免影響主線程性能和避免數據丟失之間的一種妥協。
我把這三種策略的寫回時機,以及優缺點匯總在了一張表格里,以方便你隨時查看。
在這個階段,我們可以根據系統對高性能和高可靠性的需求來選擇適合的寫回策略。總結來說:
- 如果追求高性能,可以選擇No策略。
- 如果需要高可靠性保證,應選擇Always策略。
- 如果能夠容忍一定程度的數據丟失,同時希望性能受到較小影響,那么Everysec策略是一個不錯的選擇。
然而,僅僅根據系統性能需求選擇寫回策略并不能完全保障系統的順利運行。這是因為AOF以文件形式記錄接收到的所有寫命令。隨著寫命令的不斷增加,AOF文件會變得越來越大。這會引發性能問題,主要體現在以下三個方面:
- 文件系統本身對文件大小有限制,無法容納過大的文件。
- 如果AOF文件過大,繼續往其中追加命令記錄將導致效率下降。
- 在發生宕機時,AOF文件中記錄的每個命令都必須逐個重新執行以進行故障恢復。如果AOF文件過大,恢復過程將變得極其緩慢,從而影響Redis的正常運行。
因此,我們必須采取控制措施來解決AOF文件過大的問題,而AOF重寫機制就是其中一種解決方案。
日志文件太大了怎么辦?
AOF重寫機制的核心原理很簡單,它的任務是根據數據庫的當前狀態,創建一個全新的AOF文件。這意味著它需要讀取數據庫中的所有鍵值對,然后為每個鍵值對生成一條相應的寫入命令。例如,當它讀取鍵值對"testkey": "testvalue"時,重寫機制會記錄一條"set testkey testvalue"的命令。這樣,當需要進行故障恢復時,可以重新執行這些命令,以還原"testkey": "testvalue"的寫入操作。
那么,AOF重寫機制如何幫助減小日志文件的大小呢?實際上,它具有“多對一”的功能。這意味著,舊日志文件中的多個命令在重寫后的新日志中變成了一條命令。
我們知道,AOF文件以追加的方式記錄接收到的寫命令。當一個鍵值對經歷多次修改時,AOF文件會記錄多條相應的寫命令。然而,在重寫過程中,根據鍵值對的當前狀態,只生成一條對應的寫入命令。這意味著,一個鍵值對在重寫后的日志中只需要一條命令,而在日志恢復時,只需執行這一條命令,即可完全還原該鍵值對的寫入操作。這種方式顯著減小了AOF文件的體積,提高了性能。
下面這張圖就是一個例子:
AOF重寫減少日志大小
當我們對一個列表先后做了 6 次修改操作后,列表的最后狀態是[“D”, “C”, “N”],此時,只用 LPUSH u:list “N”, “C”, "D"這一條命令就能實現該數據的恢復,這就節省了五條命令的空間。對于被修改過成百上千次的鍵值對來說,重寫能節省的空間當然就更大了。
不過,雖然 AOF 重寫后,日志文件會縮小,但是,要把整個數據庫的最新數據的操作日志都寫回磁盤,仍然是一個非常耗時的過程。這時,我們就要繼續關注另一個問題了:重寫會不會阻塞主線程?
AOF 重寫會阻塞嗎?
和 AOF 日志由主線程寫回不同,重寫過程是由后臺線程 bgrewriteaof 來完成的,這也是為了避免阻塞主線程,導致數據庫性能下降。
我把重寫的過程總結為“一個拷貝,兩處日志”。
“一個拷貝”就是指,每次執行重寫時,主線程 fork 出后臺的 bgrewriteaof 子進程。此時,fork 會把主線程的內存拷貝一份給 bgrewriteaof 子進程,這里面就包含了數據庫的最新數據。然后,bgrewriteaof 子進程就可以在不影響主線程的情況下,逐一把拷貝的數據寫成操作,記入重寫日志。
“兩處日志”又是什么呢?
因為主線程未阻塞,仍然可以處理新來的操作。此時,如果有寫操作,第一處日志就是指正在使用的 AOF 日志,Redis 會把這個操作寫到它的緩沖區。這樣一來,即使宕機了,這個 AOF 日志的操作仍然是齊全的,可以用于恢復。
而第二處日志,就是指新的 AOF 重寫日志。這個操作也會被寫到重寫日志的緩沖區。這樣,重寫日志也不會丟失最新的操作。等到拷貝數據的所有操作記錄重寫完成后,重寫日志記錄的這些最新操作也會寫入新的 AOF 文件,以保證數據庫最新狀態的記錄。此時,我們就可以用新的 AOF 文件替代舊文件了。
AOF非阻塞的重寫過程
總結來說,每次 AOF 重寫時,Redis 會先執行一個內存拷貝,用于重寫;然后,使用兩個日志保證在重寫過程中,新寫入的數據不會丟失。而且,因為 Redis 采用額外的線程進行數據重寫,所以,這個過程并不會阻塞主線程。
小結
這篇文章詳細介紹了Redis中用于防止數據丟失的AOF(Append-Only File)方法。這種方法通過逐一記錄執行操作命令的方式,以確保在需要數據恢復時能夠逐一執行這些命令,從而保證了數據的可靠性。
雖然這一方法看似相對簡單,但它充分考慮了對Redis性能的潛在影響。總結來看,AOF日志提供了三種不同的寫回策略,分別為Always、Everysec和No。這三種策略在可靠性方面從高到低排序,但在性能方面則正好相反,從低到高。
此外,為了避免AOF日志文件變得過大,Redis還引入了AOF重寫機制。該機制通過后臺線程根據數據庫內數據的最新狀態生成相應的插入命令,作為新的AOF日志。這個過程的設計避免了對主線程的阻塞,從而提高了系統的整體性能。
這三種寫回策略突顯了系統設計中的重要原則,即權衡。這意味著需要在性能和可靠性之間找到平衡。我相信這一原則對于系統設計和開發來說至關重要,希望你能深刻理解它,并在實際開發中應用得當。
然而,需要注意的是,數據持久化和AOF重寫機制主要在"記錄日志"過程中發揮作用。例如,選擇合適的數據持久化時機可以避免在記錄日志時阻塞主線程,而AOF重寫機制則可以防止AOF日志文件無限增長。但在"使用日志"的過程中,即使用AOF進行故障恢復時,所有操作記錄仍然需要逐一執行。考慮到Redis的單線程設計,這個"重放"過程可能會比較慢。
然而,是否有一種方法既能確保數據不丟失,又能更快地進行數據恢復呢?當然,這就是RDB快照的用武之地。在下一篇文章中,我們將深入研究RDB快照的工作原理和應用。