詳解Redis持久化(RDB和AOF)

詳解Redis持久化(RDB和AOF)

什麼是Redis持久化?

Redis讀寫速度快、性能優越是因為它將所有數據存在了記憶體中,然而,當Redis進程退出或重啟後,所有數據就會丟失。所以我們希望Redis能保存數據到硬碟中,在Redis服務重啟之後,原來的數據能夠恢復,這個過程就叫持久化。

Redis持久化的兩種方式?RDB和AOF

AOF:會將每次執行的命令及時保存到硬碟中,實時性更好,丟失的數據更少

RDB:會根據指定的規則定時將記憶體中的數據保存到硬碟中。

通常兩種方式結合使用,下面詳細介紹RDB和AOF

AOF

Append Only File,AOF會保存伺服器執行的所有寫操作到日誌文件中,在服務重啟以後,會執行這些命令來恢複數據。

日誌文件默認為appendonly.aof,Redis以Redis協議格式將命令保存至aof日誌文件末尾,aof文件還會被重寫,使aof文件的體積不會大於保存數據集狀態所需要的實際大小

默認情況下,aof沒有被開啟。需要在redis.conf開啟

appendonly yes  

日誌文件名

appendfilename "appendonly.aof"  

日誌文件所在目錄(RDB日誌文件也是在這裡)

dir ./  

fsync持久化策略

appendfsync everysec  

always:命令寫入aof_buf後立即調用系統fsync操作同步到AOF文件,fsync完成後執行緒返回。這種情況下,每次有寫命令都要同步到AOF文件,硬碟IO成為性能瓶頸,Redis只能支援大約幾百TPS寫入,嚴重降低了Redis的性能;即便是使用固態硬碟(SSD),每秒大約也只能處理幾萬個命令,而且會大大降低SSD的壽命。

no:命令寫入aof_buf後調用系統write操作,不對AOF文件做fsync同步;同步由作業系統負責,通常同步周期為30秒。這種情況下,文件同步的時間不可控,且緩衝區中堆積的數據會很多,數據安全性無法保證。

everysec:命令寫入aof_buf後調用系統write操作,write完成後執行緒返回;fsync同步文件操作由專門的執行緒每秒調用一次。everysec是前述兩種策略的折中,是性能和數據安全性的平衡,因此是Redis的默認配置,也是我們推薦的配置。

其餘參數:

● no-appendfsync-on-rewrite no:在重寫 AOF 文件的過程中,是否禁止 fsync。如果這個參數值設置為 yes(開啟),則可以減輕重寫 AOF 文件時 CPU 和硬碟的負載,但同時可能會丟失重寫 AOF 文件過程中的數據;需要在負載與安全性之間進行平衡。

● auto-aof-rewrite-percentage 100:指定 Redis 重寫 AOF 文件的條件,默認為 100,它會對比上次生成的 AOF 文件大小。如果當前 AOF 文件的增長量大於上次 AOF 文件的 100%,就會觸發重寫操作;如果將該選項設置為 0,則不會觸發重寫操作。

● auto-aof-rewrite-min-size 64mb:指定觸發重寫操作的 AOF 文件的大小,默認為 64MB。如果當前 AOF 文件的大小低於該值,此時就算當前文件的增量比例達到了 auto-aof-rewrite-percentage 選項所設置的條件,也不會觸發重寫操作。換句話說,只有同時滿足以上這兩個選項所設置的條件,才會觸發重寫操作。

● auto-load-truncated yes:當 AOF 文件結尾遭到損壞時,Redis 在啟動時是否仍載入 AOF 文件。

AOF持久化的實現

(1)命令追加(append):Redis 伺服器每執行一條寫命令,這條寫命令都會被追加到快取區 aof_buf 中。(避免每次執行的命令都直接寫入硬碟中,會導致硬碟 I/O 的負載過大,使得性能下降。)

命令追加的格式使用 Redis 命令請求的協議格式

file

(2)AOF 持久化文件寫入(write)和文件同步(sync):根據 appendfsync 參數設置的不同的同步策略,將快取區 aof_buf 中的數據內容同步到硬碟中。

先了解作業系統的 write 和 fsync 函數。為了提高文件的寫入效率,當用戶調用 write 函數將數據寫入文件中時,作業系統會將這些數據暫存到一個記憶體快取區中,當這個快取區被填滿或者超過了指定時限後,才會將快取區中的數據寫入硬碟中,這樣做既提高了效率,又保證了安全性。

Redis 的伺服器進程是一個事件循環(loop),這個事件循環中的文件事件負責接收客戶端的命令請求,處理之後,向客戶端發送命令回復;而其中的時間事件則負責執行像 serverCron 函數這樣需要定時運行的函數。

伺服器在處理文件事件時,可能會執行客戶端發送過來的寫命令,使得一些命令被追加到快取區 aof_buf 中。因此,在伺服器每次結束一個事件循環之前,都會調用 flushAppendOnlyFile 函數,來決定是否將快取區 aof_buf 中的數據寫入和保存到 AOF 文件中。

flushAppendOnlyFile 函數的運行與伺服器配置的 appendfsync 參數有關。

AOF文件的重寫

AOF文件定期重寫,以達到壓縮的目的。

AOF文件過於龐大,會影響Redis的寫入速度,在執行數據恢復時,也非常滿。AOF文件重寫就是解決這個問題。

AOF 文件重寫就是把 Redis 進程內的數據轉化為寫命令,然後同步到新的 AOF 文件中。在重寫的過程中,Redis 伺服器會創建一個新的 AOF 文件來替代現有的 AOF 文件,新、舊兩個 AOF 文件所保存的資料庫狀態相同,但是新的 AOF 文件不會包含冗餘命令。

AOF 文件重寫並不會對舊的 AOF 文件進行讀取、寫入操作,這個功能是通過讀取伺服器當前的資料庫狀態來實現的。

舉例說明:

​ 比如Redis使用五條rpush命令分別插入五種顏色

rpush color 〝yellow〝  rpush color 〝green〝  ...  

​ AOF重寫後

 RPUSH color 〝yellow〝 〝green〝 〝black〝 〝pink〝〝white〝  

所以AOF重寫的原理:

● AOF 文件重寫功能會丟棄過期的數據,也就是過期的數據不會被寫入 AOF 文件中。

● AOF 文件重寫功能會丟棄無效的命令,無效的命令將不會被寫入 AOF 文件中。無效命令包括重複設置某個鍵值對時的命令、刪除某些數據時的命令等。

● AOF 文件重寫功能可以將多條命令合併為一條命令,然後寫入 AOF 文件中。

怎麼觸發AOF重寫?

● 手動觸發:執行 BGREWRITEAOF 命令觸發 AOF 文件重寫。該命令與 BGSAVE 命令相似,都是啟動(fork)子進程完成具體的工作,且都在啟動時阻塞。

● 自動觸發:自動觸發 AOF 文件重寫是通過設置 Redis 配置文件中 auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size 參數的值,以及 aof_current_size 和 aof_base_size 狀態來確定何時觸發的。

auto-aof-rewrite-percentage 參數是在執行 AOF 文件重寫時,當前 AOF 文件的大小(aof_current_size)和上一次 AOF 文件重寫時的大小(aof_base_size)的比值,默認為 100。

auto-aof-rewrite-min-size 參數設置了執行 AOF 文件重寫時的最小體積,默認為 64MB。

AOF文件的後台重寫:

AOF重寫執行大量的寫入操作,就會使得這個函數的執行緒被長時間阻塞。Redis 伺服器使用單執行緒來處理命令請求。如果讓伺服器直接調用 aof_rewrite 重寫函數,那麼在 AOF 文件重寫期間,伺服器將不能繼續執行其他命令,就會一直處於阻塞狀態。

Redis 將 AOF 文件重寫程式放到了一個子進程中執行,這樣做的好處是:

● 子進程在執行 AOF 文件重寫的過程中,Redis 伺服器進程可以繼續處理新的命令請求。

● 子進程帶有伺服器進程的數據副本,使用子進程可以在使用鎖的情況下,保證數據的安全性。

使用子進程會導致資料庫狀態不一致,原因是:當子進程進行 AOF 文件重寫的時候,Redis 伺服器可以繼續執行來自客戶端的命令請求,就會有新的命令對現有資料庫狀態進行修改,進而使得伺服器當前的資料庫狀態與重寫的 AOF 文件所保存的資料庫狀態不一致。

為了解決使用子進程導致資料庫狀態不一致的問題,Redis 伺服器設置了一個 AOF 文件重寫快取區。這個 AOF 文件重寫快取區在伺服器創建子進程之後開始使用,可以利用它來解決資料庫狀態不一致的問題。當 Redis 伺服器成功執行完一條寫命令後,它會同時將這條寫命令發送給 AOF 文件快取區(aof_buf)和 AOF 文件重寫快取區。

子進程在執行 AOF 文件重寫的過程中,伺服器進程的執行過程如下:

(1)伺服器接收到來自客戶端的命令請求,並成功執行。

(2)伺服器將執行後的寫命令轉化為對應的協議格式,然後追加到 AOF 文件快取區(aof_buf)中。

(3)伺服器再將執行後的寫命令追加到 AOF 文件重寫快取區中。

file
有了 AOF 文件重寫快取區,就可以保證資料庫狀態的一致性。AOF 文件快取區的內容會被定期寫入和同步到 AOF 文件中,AOF 文件的寫入和同步不會因為 AOF 文件重寫快取區的引入而受到影響。當伺服器創建子進程之後,伺服器執行的所有寫命令都會同時被追加到 AOF 文件快取區和 AOF 文件重寫快取區中。

如果子進程完成了 AOF 文件重寫的工作,它就會發送一個完成訊號給父進程。當父進程接收到這個訊號後,就會調用訊號處理函數,繼續執行以下工作:

(1)將 AOF 文件重寫快取區中的所有內容寫入新的 AOF 文件中。新的 AOF 文件所保存的資料庫狀態與伺服器當前的資料庫狀態保持一致。

(2)修改新的 AOF 文件的文件名,新生成的 AOF 文件將會覆蓋現有(舊)的 AOF 文件,完成新、舊兩個文件的互換。

在完成上述兩個步驟之後,就完成了一次 AOF 文件後台重寫工作。

在整個 AOF 文件後台重寫的過程中,只有在訊號處理函數執行的過程中,伺服器進程才會被阻塞,在其他時候不存在阻塞情況。

AOF文件恢複數據的過程

(1)創建一個偽客戶端,用於執行 AOF 文件中的寫命令。這個偽客戶端是一個不帶網路連接的客戶端。因為只能在客戶端的上下文中才能執行 Redis 的命令,而在 AOF 文件中包含了 Redis 伺服器啟動載入 AOF 文件時所使用的所有命令,而不是網路連接,所以伺服器創建了一個不帶網路連接的偽客戶端來執行 AOF 文件中的寫命令。

(2)讀取 AOF 文件中的數據,分析並提取出 AOF 文件所保存的一條寫命令。

(3)使用偽客戶端執行被讀取出的寫命令。

(4)重複執行步驟(2)和(3),直到將 AOF 文件中的所有命令讀取完畢,並成功執行為止。這個過程完成之後,就可以將伺服器的資料庫狀態還原為關閉之前的狀態。

file
如果在 Redis 伺服器啟動載入 AOF 文件時,發現 AOF 文件被損壞了,那麼伺服器會拒絕載入這個 AOF 文件,以此來確保數據的一致性不被破壞。而 AOF 文件被損壞的原因可能是程式正在對 AOF 文件進行寫入與同步時,伺服器出現停機故障。如果 AOF 文件被損壞了,則可以通過以下方法來修復。

● 及時備份現有 AOF 文件。

● 利用 Redis 自帶的 redis-check-aof 程式,對原來的 AOF 文件進行修復,命令如下:

● 使用 diff-u 來對比原始 AOF 文件和修復後的 AOF 文件,找出這兩個文件的不同之處。

● 修復 AOF 文件之後,重啟 Redis 伺服器重新載入,進行數據恢復。

AOF持久化的優劣

● 使用 AOF 持久化會讓 Redis 持久化更長:通過設置不同的 fsync 策略來達到更長的持久化。具體有 3 種策略。

● 兼容性比較好:AOF 文件是一個日誌文件,它的作用是記錄伺服器執行的所有寫命令。當文件因為某條寫命令寫入失敗時,可以使用 redis-check-aof 進行修復,然後繼續使用。

● 支援後台重寫:當 AOF 文件的體積過大時,在後台可以自動地對 AOF 文件進行重寫,因此資料庫當前狀態的所有命令集合都會被重寫到 AOF 文件中。重寫完成後,Redis 就會切換到新的 AOF 文件,繼續執行寫命令的追加操作。

● AOF 文件易於讀取和載入:AOF 文件保存了對資料庫的所有寫命令,這些寫命令採用 Redis 協議格式追加到 AOF 文件中,因此非常容易讀取和載入。

AOF 持久化具有以下缺點:

● AOF 文件的體積會隨著時間的推移逐漸變大,導致在載入時速度會比較慢,進而影響資料庫狀態的恢復速度,性能快速下降。

● 根據所使用的 fsync 策略,使用 AOF 文件恢複數據的速度可能會慢於使用 RDB 文件恢複數據的速度。

● 因為 AOF 文件的個別命令,可能會導致在載入時失敗,從而無法進行數據恢復。

RDB

RDB 持久化生成的 RDB 文件是一個經過壓縮的二進位文件,也可以稱之為快照文件,通過該文件可以還原生成 RDB 文件時的資料庫狀態

在指定的時間間隔內,Redis 會自動將記憶體中的所有數據生成一份副本並存儲在硬碟上,這個過程就是「快照」。

快照怎麼生成

● 根據 Redis 配置文件 redis.conf 中的配置自動進行快照

save 900 1  save 300 10  save 60 10000  save m n //時間 m 和被修改的鍵的個數 n  

當在時間 m 內被修改的鍵的個數大於 n 時,就會觸發 BGSAVE 命令,伺服器就會自動執行快照操作。

Redis 的 save m n 命令是通過 serverCron 函數、dirty 計數器及 lastsave 時間戳來實現的。

serverCron 函數:這是 Redis 伺服器的周期性操作函數,默認每隔 100 毫秒執行一次,它主要的作用是維護伺服器的狀態。其中一項工作就是判斷 save m n 配置的條件是否滿足,如果滿足就會觸發執行 BGSAVE 命令。

dirty 計數器:這是 Redis 伺服器維持的一種狀態,它主要用於記錄上一次執行 SAVE 或 BGSAVE 命令後,伺服器進行了多少次狀態修改(執行添加、刪除、修改等操作);當 SAVE 或 BGSAVE 命令執行完成後,伺服器會將 dirty 重新設置為 0。dirty 計數器記錄的是伺服器進行了多少次狀態修改,而不是客戶端執行了多少次修改數據的命令。

lastsave 時間戳:主要用於記錄伺服器上一次成功執行 SAVE 或 BGSAVE 命令的時間,它是 Redis 伺服器維持的一種狀態。

dirty 計數器和 lastsave 時間戳屬性都保存在伺服器狀態的 redisServer 結構中。

save m n 命令的實現原理:伺服器每隔 100 毫秒執行一次 serverCron 函數;serverCron 函數會遍歷 save m n 配置的保存條件,判斷是否滿足。如果有一個條件滿足,就會觸發執行 BGSAVE 命令,進行快照保存。

對於每個 save m n 條件,只有以下兩個條件同時滿足才算滿足:

➢ 當前伺服器時間減去 lastsave 時間戳大於 m。

➢ 當前 dirty 計數器的個數大於等於 n。

● 用戶在客戶端執行 SAVE 或 BGSAVE 命令時會觸發快照(手動觸發)。

● 如果用戶定義了自動快照條件,則執行 FLUSHALL 命令也會觸發快照。

當執行 FLUSHALL 命令時,會清空資料庫中的所有數據。如果用戶定義了自動快照條件,則在使用 FLUSHALL 命令清空資料庫的過程中,就會觸發伺服器執行一次快照。

● 如果用戶為 Redis 設置了主從複製模式,從節點執行全量複製操作,則主節點會執行 BGSAVE 命令,將生產的 RDB 文件發送給從節點完成快照操作。

快照的實現過程

(1)Redis 調用執行 fork 函數複製一份當前進程(父進程)的副本(子進程),也就是同時擁有父進程和子進程。

(2)父進程與子進程各自分工,父進程繼續處理來自客戶端的命令請求,而子進程則將記憶體中的數據寫到硬碟上的一個臨時 RDB 文件中。

(3)當子進程把所有數據寫完後,也就表示快照生成完畢,此時舊的 RDB 文件將會被這個臨時 RDB 文件替換,這箇舊的 RDB 文件也會被刪除。這個過程就是一次快照的實現過程。

當 Redis 調用執行 fork 函數時,作業系統會使用寫時複製策略。也就是在執行 fork 函數的過程中,父、子進程共享同一記憶體數據,當父進程要修改某個數據時(執行一條寫命令),作業系統會將這個共享記憶體數據另外複製一份給子進程使用,以此來保證子進程的正確運行。因此,新的 RDB 文件存儲的是執行 fork 函數過程中的記憶體數據。

寫時複製策略也保證了在執行 fork 函數的過程中生成的兩份記憶體副本在記憶體中的佔用量不會增加一倍。但是,在進行快照的過程中,如果寫操作比較多,就會造成 fork 函數執行前後數據差異較大,此時會使得記憶體使用量變大。因為記憶體中不僅保存了當前資料庫數據,還會保存 fork 過程中的記憶體數據。

在進行快照生成的過程中,Redis 不會修改 RDB 文件。只有當快照生成後,舊的 RDB 文件才會被臨時 RDB 文件替換,同時舊的 RDB 文件會被刪除。在整個過程中,RDB 文件是完整的,因此我們可以使用 RDB 文件來實現 Redis 資料庫的備份。

RDB 文件

默認情況下,Redis 將資料庫快照保存在名為 dump.rdb 的文件中。這個文件可以修改,即AOF的dir和dbfilename 屬性

RDB 文件結構:

img

在 RDB 文件結構中,通常使用大寫字母表示常量,使用小寫字母表示變數和數據。

● REDIS 常量:該常量位於 RDB 文件的頭部,它保存著「REDIS」5 個字元,它的長度是 5 位元組。在 Redis 伺服器啟動載入文件時,程式會根據這 5 個字元判斷載入的文件是不是 RDB 文件。

● db_version 常量:該常量用於記錄 RDB 文件的版本號,它的值是一個用字元串表示的整數,占 4 位元組,注意區分它不是 Redis 的版本號。

● databases 數據:它包含 0 個或多個資料庫,以及各個資料庫中的鍵值對數據。

如果它包含 0 個資料庫,也就是伺服器的資料庫狀態為空,那麼 databases 也是空的,其長度為 0 位元組;如果它包含多個資料庫,也就是伺服器的資料庫狀態不為空,那麼 databases 不為空,根據它所保存的鍵值對的數量、類型和內容不同,其長度也是不一樣的。

如果 databases 不為空,則 RDB 文件結構如圖

img

其中,SELECTDB 是一個常量,表示其後的資料庫編號,這裡的 0 和 1 是資料庫編號。

pairs 數據:它存儲了具體的鍵值對資訊,包括鍵(key)、值(value)、數據類型、內部編碼、過期資訊、壓縮資訊等。

SELECT 0 pairs 表示 0 號資料庫;SELECT 1 pairs 表示 1 號資料庫。當資料庫中有鍵值對時,RDB 文件才會記錄該資料庫的資訊;而如果資料庫中沒有鍵值對,這一部分就會被 RDB 文件省略。

● EOF 常量:該常量是一個結束標誌,它標誌著 RDB 文件的正文內容結束,其長度為 1 位元組。在載入 RDB 文件時,如果遇到 EOF 常量,則表示資料庫中的所有鍵值對都已經載入完畢。

● check_sum 變數:該變數用於保存一個校驗和,這個校驗和是通過對 REDIS、db_version、databases、EOF 4 部分的內容進行計算得出的,是一個無符號整數,其長度為 8 位元組。

當伺服器載入 RDB 文件時,會將 check_sum 變數中保存的校驗和與載入數據時所計算出來的校驗和進行比對,從而判斷載入的 RDB 文件是否被損壞,或者是否有錯誤。

RDB文件壓縮

在默認情況下,Redis 伺服器會自動對 RDB 文件進行壓縮。在 Redis 配置文件 redis.conf 中,默認開啟壓縮。配置如下:

img

Redis 採用 LZF 演算法進行 RDB 文件壓縮。在壓縮 RDB 文件時,不要誤認為是壓縮整個 RDB 文件。實際上,對 RDB 文件的壓縮只是針對資料庫中的字元串進行的,並且只有當字元串達到一定長度(20 位元組)時才會進行壓縮。

RDB文件的創建

SAVE 命令: 會阻塞 Redis 伺服器進程,此時 Redis 伺服器將不能繼續執行其他命令請求,直到 RDB 文件創建完畢為止。

BGSAVE 命令: 會派生出一個子進程,交由子進程將記憶體中的數據保存到硬碟中,創建 RDB 文件;而 BGSAVE 命令的父進程可以繼續處理來自客戶端的命令請求。

執行 BGSAVE 命令會返回 Background saving started 資訊,但我們並不能確定 BGSAVE 命令是否已經成功執行,此時可以使用 LASTSAVE 命令來查看相關資訊。

執行 LASTSAVE 命令返回一個 UNIX 格式的時間戳,表示最近一次 Redis 成功將數據保存到硬碟中的時間。

RDB文件的載入

RDB 文件只有在啟動伺服器的時候才會被載入。當啟動伺服器時,它會檢查 RDB 文件是否存在,如果存在,就會自動載入 RDB 文件。除此之外,RDB 文件不會被載入,因為 Redis 中沒有提供用於載入 RDB 文件的命令。

那麼先載入AOF文件還是RDB文件呢?

● 如果在 Redis 配置文件中開啟了 AOF 持久化(appendonly yes),那麼在啟動伺服器的時候會優先載入 AOF 文件來還原資料庫狀態。

● 如果在 Redis 配置文件中關閉了 AOF 持久化(appendonly no),那麼在啟動伺服器的時候會優先載入 RDB 文件來還原資料庫狀態。

RDB文件的配置

除了save m n、 dbfilename dump.rdb、 dir./ 還有

● stop-writes-on-bgsave-error yes:當執行 BGSAVE 命令出現錯誤時,Redis 是否終止執行寫命令。參數的值默認被設置為 yes,表示當硬碟出現問題時,伺服器可以及時發現,及時避免大量數據丟失;當設置為 no 時,就算執行 BGSAVE 命令發生錯誤,伺服器也會繼續執行寫命令;當對 Redis 伺服器的系統設置了監控時,建議將該參數值設置為 no。

● rdbcompression yes:是否開啟 RDB 壓縮文件,默認為 yes 表示開啟,不開啟則設置為 no。

● rdbchecksum yes:是否開啟 RDB 文件的校驗,在伺服器進行 RDB 文件的寫入與讀取時會用到它。默認設置為 yes。如果將它設置為 no,則在伺服器對 RDB 文件進行寫入與讀取時,可以提升性能,但是無法確定 RDB 文件是否已經被損壞。

RDB 持久化的優劣

● RDB 文件是一個經過壓縮的二進位文件,文件緊湊,體積較小,非常適用於進行資料庫數據備份。

● RDB 持久化適用於災難恢復,而且恢複數據時的速度要快於 AOF 持久化。

● Redis 採用 RDB 持久化可以很大程度地提升性能。父進程在保存 RDB 文件時會啟動一個子進程,將所有與保存相關的功能交由子進程處理,而父進程可以繼續處理其他相關的操作。

RDB 持久化具有以下缺點:

● 在伺服器出現故障時,如果沒有觸發 RDB 快照執行,那麼它可能會丟失大量數據。RDB 快照的持久化方式決定了必然做不到實時持久化,會存在大量數據丟失。

● 當數據量非常龐大時,在保存 RDB 文件的時候,伺服器會啟動一個子進程來完成相關的保存操作。這項操作比較耗時,將會佔用太多 CPU 時間,從而影響伺服器的性能。

● RDB 文件存在兼容性問題,老版本的 Redis 不支援新版本的 RDB 文件。