Redis的持久化設計

JonPan發表於2020-06-14

Redis 持久化設計

持久化的功能:Redis是記憶體資料庫,資料都是儲存在記憶體中的,為了避免程式退出導致資料的永久丟失,要定期將Redis中的資料以某種形式從記憶體儲存到硬碟,當下次Reids重啟時,利用持久化檔案實現資料恢復。

RDB:將當前資料儲存到硬碟

AOF:將每次執行的寫命令儲存到硬碟(類似MySQL的binlog)

1. RDB持久化

RDB持久化是將當前程式中的資料生成快照儲存到硬碟(因此也稱作快照持久化),儲存的檔案字尾是rdb;當Redis重新啟動時,可以讀取快照檔案恢復資料。

  1. 觸發條件

    • 手動觸發 save 命令和bgsave命令都可以生成RDB檔案, save命令會阻塞Redis服務程式,知道RDB檔案建立完畢,bgsave命令則是建立一個子程式,由子程式來負責建立RDB檔案,父程式繼續處理請求,bgsave命令執行過程中,只有fork子程式時會阻塞伺服器,而對於save命令,整個過程都會阻塞伺服器,因此save已基本被廢棄,線上環境要杜絕save的使用;後文中也將只介紹bgsave命令。此外,在自動觸發RDB持久化時,Redis也會選擇bgsave而不是save來進行持久化

    SAVE 執行期間,AOF 寫入可以在後臺執行緒進行,BGREWRITEAOF 可以在子程式進行,所以這三種操作可以同時進行 ,為了避免效能問題,BGSAVE 和 BGREWRITEAOF 不能同時執行

  2. 自動觸發

save m n

在配置檔案中通過 save m n 命令,指定當前m秒內發生n次變化時,觸發bgsave。

​ 其中save 900 1的含義是:當時間到900秒時,如果redis資料發生了至少1次變化,則執行bgsave;save 300 10和save 60 10000同理。當三個save條件滿足任意一個時,都會引起bgsave的呼叫.

Redis的save m n,是通過serverCron函式、dirty計數器、和lastsave時間戳來實現的-

  • serverCron函式,是Redis伺服器的週期性操作函式,預設每隔100ms執行一次,該函式對伺服器的狀態進行維護,其中一項工作就是檢測save m n 配置是否滿足條件,如果滿足就執行bgsave.
  • dirty計數器 記錄伺服器進行了多少起操作,修改,不是客戶端執行了多少修改資料的命令
  • lastsave時間戳也是Reids伺服器維持的一個狀態,記錄上一次成功執行bgsave的時間

save m n的原理如下:每隔100ms,執行serverCron函式;在serverCron函式中,遍歷save m n配置的儲存條件,只要有一個條件滿足,就進行bgsave。對於每一個save m n條件,只有下面兩條同時滿足時才算滿足:

  • 當前時間-lastsave > m

  • dirty >= n

在主從複製場景下,如果從節點執行全量複製操作,則主節點會執行bgsave命令,並將rdb檔案傳送給從節點。

在執行shutdown命令時,自動執行rdb持久化

1.2 RDB檔案

設定儲存路徑

- 配置檔案:dir配置指定目錄,dbfilename指定檔名。預設是Redis根目錄下的dump.rdb檔案
- 動態設定: 

config set dir {newdir} /// config set dbfilename {newFileName}

RDB檔案 是經過壓縮的二進位制檔案,預設採用LZF演算法對RDB檔案進行壓縮,雖然壓縮耗時,但是可以大大減小檔案體積,預設是開啟的,可以通過命令關閉:

config set rdbcompression no

RDB檔案的壓縮並不是針對整個檔案進行的,而是對資料庫中的字串進行的,且只有在字串達到一定長度(20位元組)時才會進行

格式:

欄位說明:

  1. REDIS常量,儲存‘REDIS'5個字元

  2. db_version RDB檔案的版本號

  3. SELECTDB 表示一個完整的資料庫(0號資料庫),同理SELECTDB 3 pairs表示完整的3號資料庫;只有當資料庫中有鍵值對時,RDB檔案中才會有該資料庫的資訊(上圖所示的Redis中只有0號和3號資料庫有鍵值對);如果Redis中所有的資料庫都沒有鍵值對,則這一部分直接省略。其中:SELECTDB是一個常量,代表後面跟著的是資料庫號碼;0和3是資料庫號碼;

  4. KEY-VALUE-PAIRS: pairs則儲存了具體的鍵值對資訊,包括key、value值,及其資料型別、內部編碼、過期時間、壓縮資訊等等

  1. EOF 標誌著資料庫內容的結尾(不是檔案的結尾),值為 rdb.h/EDIS_RDB_OPCODE_EOF (255)

  2. CHECK-SUM RDB 檔案所有內容的校驗和,一個 uint_64t 型別值, REDIS 在寫入 RDB 檔案時將校驗和儲存在 RDB 檔案的末尾,當讀取時,根據它的值對內容進行校驗

。如果這個域的值為 0 ,那麼表示 Redis 關閉了校驗和功能。

1.3 啟動時載入

​ RDB檔案的載入工作是在伺服器啟動時自動執行的,並沒有專門的命令。但是由於AOF的優先順序更高,因此當AOF開啟時,Redis會優先載入AOF檔案來恢復資料;只有當AOF關閉時,才會在Redis伺服器啟動時檢測RDB檔案,並自動載入。伺服器載入RDB檔案期間處於阻塞狀態,直到載入完成為止

2. AOF持久化

AOF(Append Only File) 則以協議文字的方式,將所有對資料庫進行過寫入的命令(及其引數)記錄到 AOF
檔案,以此達到記錄資料庫狀態的目的

2.1 開啟AOF

Redis伺服器預設開啟RDB,關閉AOF;要開啟AOF,需要在配置檔案中配置:

appendonly yes

2.2 執行流程

2.2.1 命令寫入緩衝區

//緩衝區的定義 是一個SDS, 可以相容C語言的字串
struct redisServer {
    // AOF緩衝區, 在進入事件loop之前寫入
    sds aof_buf;
};
  1. 命令傳播: Redis將執行完的命令、命令的引數、命令的引數個數等資訊傳送到 AOF 程式中

  2. 快取追加: AOF程式根據接收到的命令命令資料,將命令轉換為網路通訊協議的格式,然後將協議內容追加到伺服器的 AOF 快取中。

    • 將命令以文字協議格式儲存在快取中
    • 為什麼使用文字協議格式?相容性,避免二次開銷,可讀性
    • 為什麼寫入快取?這樣不會受制於磁碟的IO效能,避免每次有寫命令都直接寫入硬碟,導致硬碟IO成為Redis負載的瓶頸
  3. 檔案寫入和儲存:AOF 快取中的內容被寫入到 AOF 檔案末尾,如果設定的 AOF 儲存
    條件被滿足的話,fsync 函式或者 fdatasync 函式會被呼叫,將寫入的內容真正地儲存到磁碟中。

    為了提高檔案寫入效率,在現代作業系統中,當使用者呼叫write函式將資料寫入檔案時,作業系統通常會將資料暫存到一個記憶體緩衝區裡,當緩衝區被填滿或超過了指定時限後,才真正將緩衝區的資料寫入到硬碟裡。這樣的操作雖然提高了效率,但也帶來了安全問題:如果計算機停機,記憶體緩衝區中的資料會丟失;因此係統同時提供了fsync、fdatasync等同步函式,可以強制作業系統立刻將緩衝區中的資料寫入到硬碟裡,從而確保資料的安全性。

    AOF儲存模式:

    • AOF_FSYNC_ALWAYS: 命令寫入aof-buf後立即呼叫系統的fsync操作同步到AOF檔案。因為 SAVE 是由 Redis 主程式執行的,所以在 SAVE 執行期間,主程式會被阻塞,不能接受命令請求。這種情況下,每次有寫命令都要同步到AOF檔案,硬碟IO成為效能瓶頸,Redis只能支援大約幾百TPS寫入,嚴重降低了Redis的效能;即便是使用固態硬碟(SSD),每秒大約也只能處理幾萬個命令,而且會大大降低SSD的壽命。
    • AOF_FSYNC_NO: 命令寫入aof_buf後呼叫系統write操作,不對AOF檔案做fsync同步;同步由作業系統負責,通常同步週期為30秒。這種情況下,檔案同步的時間不可控,且緩衝區中堆積的資料會很多,資料安全性無法保證。
    • AOF_FSYNC_EVERYSEC: 每一秒鐘儲存一次,命令寫入aof_buf後呼叫系統write操作, write完成後執行緒返回, fsync同步檔案操作由專門執行緒每秒呼叫一次

2.2.2. 檔案重寫

隨著命令不斷寫入AOF,檔案會越來越大,為了解決這個問題,Redis引入AOF重寫機制壓縮檔案體積,AOF檔案重寫是把Redis程式內的資料轉化為寫命令同步到新AOF檔案的過程。

重寫後的AOF檔案為什麼可以變小?

  1. 程式內已經超時的資料不再寫入檔案
  2. 舊的AOF檔案含有無效命令 ,如有些資料被重複設值(set mykey v1, set mykey v2)、有些資料被刪除了(sadd myset v1, del myset)等等, 新的AOF檔案只保留最終的資料寫入命令
  3. 多條寫入命令可以合併為一個,如:lpush list a、lpush list b可以轉化為:lpush list a b。為了防止單條命令過大造成客戶端緩衝區溢位,對於list、set、hash等型別操作,以64個元素為邊界拆分為多條

AOF重寫可以手動觸發也可以自動觸發:

  • 手動觸發: 直接呼叫bgrewriteaof命令
  • 自動觸發:根據auto-aof-rewrite-min-size和auto-aof-rewrite-percentage引數確定自動觸發時機。
    • auto-aof-rewrite-min-size:表示執行AOF重寫時檔案最小體積,預設為64MB
    • auto-aof-rewrite-percentage:代表當前AOF檔案空間(aof_current_size)和上一次重寫後AOF檔案空間(aof_base_size)的比值

流程說明:

1)執行AOF重寫請求。

如果當前程式正在執行AOF重寫,請求不執行。

如果當前程式正在執行bgsave操作,重寫命令延遲到bgsave完成之後再執行。

2)父程式執行fork建立子程式,開銷等同於bgsave過程。

3.1)主程式fork操作完成後,繼續響應其它命令。

  所有修改命令依然寫入AOF檔案緩衝區並根據appendfsync策略同步到磁碟,保證原有AOF機制正確性。

3.2)由於fork操作運用寫時複製技術,子程式只能共享fork操作時的記憶體資料

  由於父程式依然響應命令,Redis使用“AOF”重寫緩衝區儲存這部分新資料,防止新的AOF檔案生成期間丟失這部分資料。

4)子程式依據記憶體快照,按照命令合併規則寫入到新的AOF檔案。

  每次批量寫入硬碟資料量由配置aof-rewrite-incremental-fsync控制,預設為32MB,防止單次刷盤資料過多造成硬碟阻塞。

5.1)新AOF檔案寫入完成後,子程式傳送訊號給父程式,父程式呼叫一個訊號處理函式,並執行以前操作更新統計資訊。

5.2)父程式把AOF重寫緩衝區的資料寫入到新的AOF檔案。這時新 AOF 檔案所儲存的資料庫狀態將和伺服器當前的資料庫狀態一致。

5.3)對新的AOF檔案進行改名,原子地(atomic)覆蓋現有的AOF檔案,完成新舊檔案的替換。

在整個 AOF 後臺重寫過程中,只有訊號處理函式執行時會對伺服器程式(父程式)造成阻塞,其他時候,AOF 後臺重寫都不會阻塞父程式,這將 AOF 重寫對伺服器效能造成的影響降到了最低

參考《Redis-設計與實現:AOF-持久化》

2.2.3 重啟載入

流程說明:

1)AOF持久化開啟且存在AOF檔案時,優先載入AOF檔案。

2)AOF關閉或者AOF檔案不存在時,載入RDB檔案。

3)載入AOF/RDB檔案成功後,Redis啟動成功。

4)AOF/RDB檔案存在錯誤時,Redis啟動失敗並列印錯誤資訊。

資料還原的詳細步驟:

  1. 建立一個不帶網路連線的偽客戶端(fake client): 因為 Redis 的命令只能在客戶端上下文中執行,而載入 AOF 檔案時所使用的命令直接來源於 AOF 檔案而不是網路連線,所以伺服器使用了一個沒有網路連線的偽客戶端來執行 AOF 檔案儲存的寫命令,偽客戶端執行命令的效果和帶網路連線的客戶端執行命令的效果完全一樣。
  2. 從AOF檔案中分析並讀取出一條寫命令,使用偽客戶端執行被讀出的寫命令,重複此操作,直到AOF檔案中的所有寫命令都被處理完畢為止。

2.2.4 檔案校驗

載入損壞的AOF檔案會拒絕啟動,並列印錯誤資訊。

注意:對於錯誤格式的AOF檔案,先進性備份,然後採用redis-check-aof --fix命令進行修復,修復後使用diff -u對比資料差異,找到丟失的資料,有些可以進行人工補全。

AOF檔案可能存在結尾不完整的情況,比如機器突然掉電導致AOF尾部檔案命令寫入不全。

Redis為我們提高了aof-load-truncated配置來相容這種情況,預設開啟

3. 瞭解MySQL中的binlog

mysql binlog應用場景與原理深度剖析

參考博文與書籍:

  1. 《redis設計與實現》
  2. Redis持久化
  3. [徐劉根-Redis實戰和核心原理詳解(8)使用快照RDB和AOF將Redis資料持久化到硬碟中](https://blog.csdn.net/xlgen157387/article/details/61925524

相關文章