詳細分析Redis的持久化操作——RDB與AOF

一、前言

  由於疫情的原因,學校還沒有開學,這也就讓我有了很多的時間。趁着時間比較多,我終於可以開始學習那些之前一直想學的技術了。最近這幾天開始學習Redis,買了本《Redis實戰》,看到了第四章,前三章都是講一些Redis的基本使用以及命令,第四章才開始涉及到原理相關的內容。《Redis實戰》的第四章涉及到了Redis的持久化、主從複製以及事務等內容,我個人認為這些應該屬於Redis中比較重要的部分,也是面試的常考內容。這篇博客就來記錄一下Redis的持久化機制。

二、正文

2.1 為什麼需要持久化

  學習過Redis的應該都知道,RedisMySQL等關係型數據庫不同,它的數據不是存儲在硬盤中,而是存放在內存,所以Redis的速度非常快。而這也就會造成一個問題:電腦如果宕機,或者由於某些原因需要重啟,此時內存中的數據就會丟失Redis既然把數據存放在內存,自然也就無法避免這個問題。所以,為了在電腦重啟後,能夠恢復原來的數據,Redis就需要提供持久化的機制,將Redis數據庫存儲在內存中的數據,在磁盤中進行備份,也就是持久化。而當Redis重啟後,去磁盤中將持久化的數據重新讀取到內存,便能避免數據的丟失。

2.2 Redis的持久化方式

  Redis提供了兩種持久化的方式,分別是:

  • 快照持久化;
  • AOF持久化;

  下面我就來詳細地介紹這兩種持久化的方式。

2.3 快照持久化(RDB)

  快照持久化也就做RDB持久化。快照持久化的實現方式簡單來說就是:Redis將當前內存中存儲的數據寫入到一個文件中,將這個文件作為Redis當前的一個快照,保存在磁盤中。當Redis重啟時,將這個快照文件中存儲的內容加載進內存,即可恢復Redis之前的狀態。默認情況下,Redis將快照保存在一個叫做dump.rdb的文件中,我們也可以在配置文件中,通過dbfilename選項來設置快照文件的名稱;默認情況下快照文件就保存在Redis的安裝目錄下,我們可以在配置文件中,通過dir選項來配置快照文件的存儲路徑(其實也是AOF的路徑)。

2.4 執行快照持久化的方式

  執行快照持久化有兩種方式:

2.4.1 使用配置文件

  第一種方式就是在Redis的配置文件中(windows下這個文件叫redis.windows-service.conf)加上save配置項,比如像下面這樣:

save 60 100:在配置文件中加上這一條的意思是,Redis會每60秒檢查一次,若在這60秒中,Redis數據庫執行了100次以上的寫操作,那Redis就會生成一個快照文件,替換原來的快照文件;若不滿足這個條件,則不生成快照,繼續等待60秒;

  我們可以根據自己的需求,調整每次等待的時間,以及對寫操作次數的要求。而且,我們可以在配置文件中,添加多個save選項,比如一個save 60 100,一個save 5 10,則Redis5秒以及每100秒都會判斷一次。需要注意的是,我們不應該讓生成快照太過頻繁,因為這是一個比較消耗資源的工作,會降低Redis的響應速度。

2.4.2 使用指令

  執行快照持久化的第二個方法就是使用Redis指令,Redis提供了兩個指令來請求服務器進行快照持久化,這兩個指令分別是SAVEBGSAVE

(a)BGSAVE指令

  BGSAVE的執行流程如下:

  1. Redis調用系統的fork(),創建出一個子進程;
  2. 子進程將當前Redis中的數據,寫入到一個臨時文件中;同時父進程不受影響,繼續執行客戶端的請求;
  3. 子進程將所有的數據寫入到了臨時文件後,於是使用這個文件替換原來的快照文件(默認是dump.rdb);

  值得一提的是,通過配置文件執行快照持久化的方式,實際上就是Redis在判斷滿足條件時,調用BGSAVE指令來實現的。

(b)SAVE指令

  SAVE指令生成快照的方式與BGSAVE不同,Redis執行SAVE指令時,不會創建一個子進程,異步的生成快照文件,而是直接使用Redis當前進程。執行SAVE指令在創建快照的過程中,Redis服務器會阻塞所有的Redis客戶端,直到快照生成完畢,並更新到磁盤之後,才會繼續執行客戶端發來的增刪改查的指令。

  當然,這個指令一般很少使用,因為會阻塞客戶端,造成停頓。但是實際上,這個指令的執行效率一般比BGSAVE更高,因為不需要創建子進程,而且在這個過程中,其他操作被阻塞,Redis服務器一心一意地生成快照。在《Redis實戰》中,作者提到了使用SAVE指令的一個案例:

  當前服務器的Redis數據庫中保存了大量數據,使用BGSAVE指令生成快照會非常的耗時 ,而且由於所剩內存不多,甚至無法創建子進程,於是作者編寫了一個shell腳本,這個腳本的內容就是讓服務器每天凌晨3點,執行SAVE命令,生成快照,這樣就不需要創建子進程,而且由於是凌晨三點,用戶較少,SAVE的阻塞機制也不會有太大的影響。

2.5 快照持久化的優缺點

(1)優點:

  1. 快照的rdb文件是一個經過壓縮的緊湊文件,它保存了Redis在某個時間點上的數據集,這個文件非常適合用來備份。我們可以存儲Redis服務器在不同時間點上的rbd文件,比如說一小時存儲一次,一個月存儲一次,這樣就可以在遇到問題或有特殊需求時,將Redis恢復到某一個時間點;

  2. RDB非常適用於災難恢復(disaster recovery):它只有一個文件,並且內容都非常緊湊,可以(在加密後)將它傳送到別的服務器上;

  3. RDB 可以最大化 Redis 的性能:父進程在保存 RDB 文件時唯一要做的就是 fork 出一個子進程,然後這個子進程就會處理接下來的所有保存工作,父進程無須執行任何磁盤I/O操作,所以不會影響父進程處理客戶端的請求;

  4. RDB 在恢復大數據集時的速度比 AOF 的恢復速度要快。

(2)缺點

  1. 當我們的服務器發生異常,導致停機時,那我們將會丟失最後一次創建快照後,所作的所有寫操作,因為這些操作發生在內存中,還沒來得及同步到磁盤。比如我們在配置文件中配置每5分鐘生成一次快照,那當系統發生故障導致宕機時,我們將會丟失好幾分鐘內的操作;
  2. 每次執行BGSAVE創建快照,都需要先創建出一個子進程,再由子進程執行後續操作,當內存中數據較大時,創建一個子進程將會非常耗時,造成服務器等待數毫秒,甚至達到一秒,影響用戶體驗。而且從這一點也可以說明,我們不能通過提高生成快照的頻率,來解決第一個缺點;

2.6 AOF持久化

  AOF全稱為只追加文件(append-only file),它的實現方式簡單來說就是:AOF持久化機制,會將Redis執行的所有寫指令,追加到到AOF的末尾,當服務器發生宕機,或者由於某些原因需要重啟時,在重啟後,讀取AOF文件,重新執行其中記錄的寫操作,以此達到恢複數據的目的。

  AOF持久化機制默認是關閉的,我們可以在配置文件中,配置appendonly yes來開啟。同時我們也可以通過配置appendfsync,控制寫操作追加到AOF中的頻率,它有如下三種選項:

  • alwaysRedis每次執行寫指令,都會立即將這個寫指令同步到AOF中;使用這個選項時,Redis發生異常,則最多只會丟失一次寫操作(也就是在同步的過程中宕機,沒同步完成),但是這也會導致Redis的響應速度變慢,因為此選項會造成頻繁的IO操作;
  • everysec(默認):每秒同步一次,使用此選項,速度足夠快,而且發生宕機時也只會丟失1s內的寫操作;
  • no:讓操作系統來決定什麼時候進行同步,這個選項速度更快,但是不安全,宕機時丟失數據的多少是不確定的;

  推薦(也是默認),使用第二個選項everysec,每秒進行一次同步,因為這個選項兼顧了速度與安全性,而第一個選項太慢,第三個選項無法保證安全性。

2.7 AOF的重寫機制

  AOF持久化的執行機制就是,不斷地將寫指令追加到AOF的末尾,這樣就會導致AOF越來越大。為了解決AOF越來越大的問題,Redis實現了一種優化機制——AOF重寫。當Redis檢查到AOF已經很大時,就會觸發重寫機制,優化其中的內容,將它優化為能夠得到相同結果的最小的指令集合。

  比如說,我們在Redis中使用SET指令創建了一個String類型的數據,最後使用DEL指令將它刪除了。如果開啟了AOF持久化,那麼則AOF中,將會記錄這條SETDEL指令。但是,在執行重寫的過程中,這個String最後被刪除了,那麼Redis就不會將這兩條指令加入新的AOF中,因為已經被刪除的數據,不需要恢復。再比如說,我們使用Redis維護了一個計數器cnt,我們使用了100INCR指令,讓cnt自增到了100,而Redis重寫AOF時,可以將這100incr修改為一條SET指令,直接將cnt設置為100,而不是保留100INCR

  經過了重寫後,AOF的大小將會大大減小,而且也去除了不必要的操作,優化了恢複數據的指令集。而AOF重寫的過程與生成一個快照文件類似,如下:

  1. Redis執行fock(),創建一個子進程用來執行後續操作,而父進程繼續處理髮送到Redis的執行請求;
  2. 子進程重寫舊AOF文件,將重寫後的內容寫入到一個臨時文件;
  3. 如果這個過程中,有新的寫指令到達,那麼Redis會將這些寫指令依舊追加到舊的AOF中,同時也會將這些指令加入到內存的一個緩衝區中。這樣做的目的是,如果服務器發生異常,AOF重寫失敗,這些指令依然能夠保存在舊AOF,不會丟失;
  4. 當子進程完成重寫工作時,它給父進程發送一個信號,父進程在接收到信號之後,將內存緩存中的所有寫指令追加到新 AOF 文件的末尾;
  5. 使用新AOF替換舊的AOF,這之後執行的所有寫指令都將追加到新的AOF中;

2.8 AOF的錯誤處理

  AOF文件是有可能發生錯誤的,比如上面提過,如果當前Redis正在向AOF中追加一個寫指令,但是此時服務器宕機,那麼這個存入AOF中的這個寫指令就是不完整的,也就是AOF出現了錯誤。如果停機造成了 AOF 文件出錯(corrupt), 那麼 Redis 在重啟時會拒絕載入這個 AOF 文件, 從而確保數據的一致性不會被破壞。

  那麼,當AOF發生了錯誤,應該如何處理呢?我們可以使用如下命令:

redis-check-aof –fix:redis-check-aof用來檢測AOF是否存在錯誤,如果指定了–fix參數,那麼Redis在檢測到AOF的錯誤後,會對AOF進行修復。

  redis-check-aof修復AOF的方式非常簡單:掃描AOF文件,尋找其中不正確或不完整的指令,當發現第一個出錯的指令後,就將這個指令以及這之後的所有指令刪除。為什麼需要刪除出錯指令之後的所有指令呢?因為當一條指令出錯,很有可能影響到後續的操作,導致後續操作的都是臟數據,而Redis無法檢測哪些指令是受到影響的,所以為了保險起見,就將後續指令全部刪除。不過不用擔心,因為在大多數情況下,出錯的都是AOF最末尾的指令。

2.9 AOF的優缺點

(1)優點:

  1. 使用 AOF 持久化會讓 Redis 變得非常耐久,意思就是說,當發生異常導致需要重啟服務器時,只會丟失很少的一部分數據,因為AOF持久化默認1s同步一次,也就是說,Redis最多只會丟失1s中所做的修改;
  2. Redis 可以在 AOF 文件體積變得過大時,自動地在後台對 AOF 進行重寫: 重寫後的新 AOF 文件包含了恢復當前數據集所需的最小命令集合。
  3. AOF 文件有序地保存了對數據庫執行的所有寫入操作, 這些寫入操作以 Redis 協議的格式保存, 因此 AOF 文件的內容非常容易被人讀懂, 對文件進行分析(parse)也很輕鬆。
  4. AOF 文件是一個只進行追加操作的日誌文件(append only log), 因此對 AOF 文件的寫入不需要進行 seek , 即使日誌因為某些原因而包含了未寫入完整的命令(比如寫入時磁盤已滿,寫入中途停機,等等), redis-check-aof 工具也可以輕易地修復這種問題。

(2)缺點:

  1. 對於相同的數據集來說,AOF 文件的體積通常要大於快照RDB文件的體積大,因為RDB只存儲數據,而AOF中的寫指令,既包含指令,也包含數據;
  2. 如果AOF體積太大,那麼恢複數據將要花費較長的時間,因為需要重做指令;

2.10 選擇快照還是AOF?

  說到這裡,可能就有人要問了,在實際生產中,我們應該使用快照持久化還是AOF持久化呢?

1、如果希望自己的數據庫有很高的安全性,則應該兩者同時使用,AOF用作精確記錄,而快照用作數據備份,前面也說過,快照非常適合用來做數據備份,因為它只存儲數據庫中的數據,並且經過了壓縮,而且它恢復Redis數據的速度一般要快於AOF

2、如果可以容忍一段時間內的數據丟失,則可以考慮只使用快照持久化,減小服務器的開銷;

  值得一提的是,如果我們同時開啟了快照持久化和AOF持久化,Redis在重啟後,會優先選擇AOF來恢複數據,因為一般情況下,AOF能夠更加完整地恢複數據。

三、總結

  快照持久化和AOF持久化各有優劣,在實際生產環境中,我們一般是兩者配合使用,快照持久化消耗較低,而且適合用於備份,但是會丟失一段時間的數據;AOF持久化更加地耐久,可靠性更高,但是開銷可能相對較高。以上就對Redis的持久化機製做了一個比較詳細的介紹,相信看完只後,對Redis會有一個更加深入的理解。

四、參考

Tags: