源碼級別瞭解 Redis 持久化

文章首發於公衆號「蘑菇睡不着」,歡迎來訪~

前言

  你們都知道 Redis 是一個內存數據庫,數據都存儲在內存中,這也是 Redis 很是快的緣由之一。雖然速度提上來了,可是若是數據一直放在內存中,是很是容易丟失的。好比 服務器關閉或宕機了,內存中的數據就木有了。爲了解決這一問題,Redis 提供了 持久化 機制。分別是 RDB 以及 AOF 持久化。c++

RDB

什麼是 RDB 持久化?

RDB 持久化能夠在指定的時間間隔內生成數據集的時間點快照(point-in-time snapshot)。
redis

RDB 的優勢?

  • RDB 是一種表示某個即時點的 Redis 數據的緊湊文件。RDB 文件適用於備份。例如,你可能想要每小時歸檔最近24小時的 RDB 文件,天天保存近30天的 RDB 快照。這容許你很容易的恢復不一樣版本的數據集以容災。
  • RDB 很是適合於災難恢復,做爲一個緊湊的單一文件,能夠被傳輸到遠程的數據中心。
  • RDB 最大化了 Redis 的性能。由於 Redis 父進程持久化時惟一須要作的是啓動(fork)一個子進程,由子進程完成全部剩餘的工做。父進程實例不須要執行像磁盤IO這樣的操做。
  • RDB 在重啓保存了大數據集的實例比 AOF 快。數據庫

    RDB 的缺點?

  • 當你須要在Redis中止工做(例如停電)時最小化數據丟失,RDB可能不太好。你能夠配置不一樣的保存點(save point)來保存RDB文件(例如,至少5分鐘和對數據集100次寫以後,可是你能夠有多個保存點)。然而,你一般每隔5分鐘或更久建立一個RDB快照,因此一旦Redis由於任何緣由沒有正確關閉而中止工做,你就得作好最近幾分鐘數據丟失的準備了。
  • RDB須要常常調用fork()子進程來持久化到磁盤。若是數據集很大的話,fork()比較耗時,結果就是,當數據集很是大而且CPU性能不夠強大的話,Redis會中止服務客戶端幾毫秒甚至一秒。AOF也須要fork(),可是你能夠調整多久頻率重寫日誌而不會有損(trade-off)持久性(durability)。

RDB 文件的建立與載入

  有個兩個 Redis 命令能夠用於生成 RDB 文件,一個是 SAVE,另外一個是 BGSAVE
  SAVE 命令會阻塞 Redis 服務器進程,直到 RDB 文件建立完畢爲止,在服務器進程阻塞期間,服務器不能處理任何命令請求。數組

> SAVE     // 一直等到 RDB 文件建立完畢
OK

和 SAVE 命令直接阻塞服務器進程不一樣的是,BGSAVE 命令會派生出一個子進程,而後由子進程負責建立 RDB 文件,服務器進程(父進程)繼續處理命令進程。
執行fork的時候操做系統(類Unix操做系統)會使用寫時複製(copy-on-write)策略,即fork函數發生的一刻父子進程共享同一內存數據,當父進程要更改其中某片數據時(如執行一個寫命令 ),操做系統會將該片數據複製一份以保證子進程的數據不受影響,因此新的RDB文件存儲的是執行fork一刻的內存數據。安全

> BGSAVE  // 派生子進程,並由子進程建立 RDB 文件
Background saving started

生成 RDB 文件由兩種方式:一種是手動,就是上邊介紹的用命令的方式;另外一種是自動的方式。
接下來詳細介紹一下自動生成 RDB 文件的流程。
Redis 容許用戶經過設置服務器配置的 save 選項,讓服務器每隔一段時間自動執行一次 BGSAVE 命令。
用戶能夠經過在 redis.conf 配置文件中的 SNAPSHOTTING 下 save 選項設置多個保存條件,但只要其中任意一個條件被知足,服務器就會執行 BGSAEVE 命令。
如,如下配置:
save 900 1
save 300 10
save 60 10000
上邊三個配置的含義是:服務器

  • 服務器在 900 秒內,對數據庫進行了至少 1 次修改。
  • 服務器在 300 秒內,對數據庫進行了至少 10 次修改。
  • 服務器在 60 秒內,對數據庫進行了至少 10000 次修改。

若是沒有手動去配置 save 選項,那麼服務器會爲 save 選項配置默認參數:
save 900 1
save 300 10
save 60 10000
接着,服務器就會根據 save 選項的配置,去設置服務器狀態 redisServer 結構的 saveparams 屬性:網絡

struct redisServer{

  // ...
  
  // 記錄了保存條件的數組
  struct saveparams *saveparams;
  
  // ...
};

saveparams 屬性是一個數組,數組中的每個元素都是一個 saveparam 結構,每一個 saveparam 結構都保存了一個 save 選項設置的保存條件:app

struct saveparam {

  // 秒數
  time_t seconds;
  
  // 修改數
  int changes;
};

除了 saveparams 數組以外,服務器狀態還維持着一個 dirty 計數器,以及一個 lastsave 屬性;async

struct redisServer {
    // ...
    
    // 修改計數器
    long long dirty;
    
    // 上一次執行保存時間
     time_t lastsave;
     
     // ...
}
  • dirty 計數器記錄距離上一次成功執行 SAVE 或 BGSAVE 命令以後,服務器對數據庫狀態(服務器中的全部數據庫)進行了多少次修改(包括寫入、刪除、更新等操做)。
  • lastsave 屬性是一個 UNIX 時間戳,記錄了服務器上一次執行 SAVE 或 BGSAVE 命令的時間。

檢查條件是否知足觸發 RDB

Redis 的服務器週期性操做函數 serverCron 默認每隔 100 毫秒執行一次,該函數用於對正在運行的服務器進行維護,它的其中一項工做就是檢查 save 選項所設置的保存條件是否已經知足,若是知足的話就執行 BGSAVE 命令。
Redis serverCron 源碼解析以下:函數

程序會遍歷並檢查 saveparams 數組中的全部保存條件,只要有任意一個條件被知足,服務器就會執行 BGSAVE 命令。
下面是 rdbSaveBackground 的源碼流程:

RDB 文件結構

下圖展現了一個完整 RDB 文件所包含的各個部分。

redis 文件的最開頭是 REDIS 部分,這個部分的長度是 5 字節,保存着 「REDIS」 五個字符。經過這五個字符,程序能夠在載入文件時,快速檢查所載入的文件是否時 RDB 文件。

db_version 長度爲 4 字節,他的值時一個字符串表示的整數,這個整數記錄了 RDB 文件的版本號,好比 「0006」 就表明 RDB 文件的版本爲第六版。

database 部分包含着零個或任意多個數據庫,以及各個數據庫中的鍵值對數據:

  • 若是服務器的數據庫狀態爲空(全部數據庫都是空的),那麼這個部分也爲空,長度爲 0 字節。
  • 若是服務器的數據庫狀態爲非空(有至少一個數據庫非空),那麼這個部分也爲非空,根據數據庫所保存鍵值對的數量、類型和內容不一樣,這個部分的長度也會有所不一樣。

EOF 常量的長度爲 1 字節,這個常量標誌着 RDB 文件正文內容的結束,當讀入程序遇到這個值後,他知道全部數據庫的全部鍵值對已經載入完畢了。

check_sum 是一個 8 字節長的無符號整數,保存着一個校驗和,這個校驗和時程序經過對 REDIS、db_version、database、EOF 四個部分的內容進行計算得出的。服務器在載入 RDB 文件時,會將載入數據所計算出的校驗和與 check_sum 所記錄的校驗和進行對比,以此來檢查 RDB 是否有出錯或者損壞的狀況。
舉個例子:下圖是一個 0 號數據庫和 3 號數據庫的 RDB 文件。第一個就是 「REDIS」 表示是一個 RDB 文件,以後的 「0006」 表示這是第六版的 REDIS 文件,而後是兩個數據庫,以後就是 EOF 結束標識符,最後就是 check_sum。

AOF 持久化

什麼是 AOF 持久化

AOF持久化方式記錄每次對服務器寫的操做,當服務器重啓的時候會從新執行這些命令來恢復原始的數據,AOF命令以redis協議追加保存每次寫的操做到文件末尾.Redis還能對AOF文件進行後臺重寫,使得AOF文件的體積不至於過大.

AOF 的優勢?

  • 使用AOF 會讓你的Redis更加耐久: 你可使用不一樣的fsync策略:無fsync,每秒fsync,每次寫的時候fsync.使用默認的每秒fsync策略,Redis的性能依然很好(fsync是由後臺線程進行處理的,主線程會盡力處理客戶端請求),一旦出現故障,你最多丟失1秒的數據.
  • AOF文件是一個只進行追加的日誌文件,因此不須要寫入seek,即便因爲某些緣由(磁盤空間已滿,寫的過程當中宕機等等)未執行完整的寫入命令,你也也可以使用redis-check-aof工具修復這些問題.
  • Redis 能夠在 AOF 文件體積變得過大時,自動地在後臺對 AOF 進行重寫: 重寫後的新 AOF 文件包含了恢復當前數據集所需的最小命令集合。 整個重寫操做是絕對安全的,由於 Redis 在建立新 AOF 文件的過程當中,會繼續將命令追加到現有的 AOF 文件裏面,即便重寫過程當中發生停機,現有的 AOF 文件也不會丟失。 而一旦新 AOF 文件建立完畢,Redis 就會從舊 AOF 文件切換到新 AOF 文件,並開始對新 AOF 文件進行追加操做。
  • AOF 文件有序地保存了對數據庫執行的全部寫入操做, 這些寫入操做以 Redis 協議的格式保存, 所以 AOF 文件的內容很是容易被人讀懂, 對文件進行分析(parse)也很輕鬆。 導出(export) AOF 文件也很是簡單: 舉個例子, 若是你不當心執行了 FLUSHALL 命令, 但只要 AOF 文件未被重寫, 那麼只要中止服務器, 移除 AOF 文件末尾的 FLUSHALL 命令, 並重啓 Redis , 就能夠將數據集恢復到 FLUSHALL 執行以前的狀態。

AOF 的缺點?

  • 對於相同的數據集來講,AOF 文件的體積一般要大於 RDB 文件的體積。
  • 根據所使用的 fsync 策略,AOF 的速度可能會慢於 RDB 。 在通常狀況下, 每秒 fsync 的性能依然很是高, 而關閉 fsync 可讓 AOF 的速度和 RDB 同樣快, 即便在高負荷之下也是如此。 不過在處理巨大的寫入載入時,RDB 能夠提供更有保證的最大延遲時間(latency)。

AOF持久化的實現

AOF持久化功能的實現能夠分爲命令追加(append)、文件寫入、文件同步(sync)三個步驟。

命令追加

當 AOF 持久化功能處於打開狀態時,服務器在執行完一個寫命令以後,會以協議格式將被執行的寫命令追加到服務器狀態的 aof_buf 緩衝區的末尾。

struct redisServer {
  // ...
  // AOF 緩衝區  
  sds aof_buf;
  
  // ..
};

若是客戶端向服務器發送如下命令:

> set KEY VALUE
OK

那麼服務器在執行這個 set 命令以後,會將如下協議內容追加到 aof_buf 緩衝區的末尾;

*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n

AOF 文件的寫入與同步

  Redis的服務器進程就是一個事件循環(loop),這個循環中的文件事件負責接收客戶端
的命令請求,以及向客戶端發送命令回覆,而時間事件則負責執行像 serverCron 函數這樣需
要定時運行的函數。
  由於服務器在處理文件事件時可能會執行寫命令,使得一些內容被追加到aof_buf緩衝區
裏面,因此在服務器每次結束一個事件循環以前,它都會調用 flushAppendOnlyFile 函數,考
慮是否須要將aof_buf緩衝區中的內容寫入和保存到AOF文件裏面,這個過程能夠用如下僞代
碼錶示:

def eventLoop():
  while True:
  
  #處理文件事件,接收命令請求以及發送命令回覆
  #處理命令請求時可能會有新內容被追加到 aof_buf緩衝區中
  processFileEvents()
  
  #處理時間事件
  processTimeEvents()
  
  #考慮是否要將 aof_buf中的內容寫入和保存到 AOF文件裏面
  flushAppendOnlyFile()

flushAppendOnlyFile函數的行爲由服務器配置的 appendfsync 選項的值來決定,各個不一樣
值產生的行爲以下表所示。

appendfsync 選項的值 flushAppendOnlyFile 函數的行爲
always 將 aof_buf 緩衝區中的全部內容寫入並同步到 AOF 文件
everysec 將 aof_buf 緩衝區中的全部內容寫入到 AOF 文件,若是上次同步 AOF 文件的時間距離如今超過一秒鐘,那麼再次對 AOF 文件進行同步,而且這個同步操做是由一個線程專門負責執行的
no 將 aof_buf 緩衝區中的全部內容寫入到 AOF 文件,但並不對 AOF 文件進行同步,什麼時候同步由操做系統來決定

若是用戶沒有主動爲appendfsync選項設置值,那麼appendfsync選項的默認值爲everysec。
寫到這裏有的小夥伴可能會對上面說的寫入和同步含義弄混,這裏說一下:
寫入:將 aof_buf 中的數據寫入到 AOF 文件中。
同步:調用 fsync 以及 fdatasync 函數,將 AOF 文件中的數據保存到磁盤中。
通俗地講就是,你要往一個文件寫東西,寫的過程就是寫入,而同步則是將文件保存,數據落到磁盤上。
你們以前看文章的時候是否是大多都說 AOF 最多丟失一秒鐘的數據,那是由於 redis AOF 默認是 everysec 策略,這個策略每秒執行一次,因此 AOF 持久化最多丟失一秒鐘的數據。

AOF 文件的載入與數據還原

由於AOF文件裏面包含了重建數據庫狀態所需的全部寫命令,因此服務器只要讀入並從新執行一遍AOF文件裏面保存的寫命令,就能夠還原服務器關閉以前的數據庫狀態。 Redis讀取AOF文件並還原數據庫狀態的詳細步驟以下:

  1. 建立一個不帶網絡鏈接的僞客戶端(fake client):由於Redis的命令只能在客戶端上 下文中執行,而載入AOF文件時所使用的命令直接來源於AOF文件而不是網絡鏈接,因此服 務器使用了一個沒有網絡鏈接的僞客戶端來執行AOF文件保存的寫命令,僞客戶端執行命令 的效果和帶網絡鏈接的客戶端執行命令的效果徹底同樣。
  2. 從AOF文件中分析並讀取出一條寫命令。
  3. 使用僞客戶端執行被讀出的寫命令。
  4. 一直執行步驟2和步驟3,直到AOF文件中的全部寫命令都被處理完畢爲止。
    當完成以上步驟以後,AOF文件所保存的數據庫狀態就會被完整地還原出來,整個過程 以下圖所示。

AOF 重寫

由於AOF持久化是經過保存被執行的寫命令來記錄數據庫狀態的,因此隨着服務器運行 時間的流逝,AOF文件中的內容會愈來愈多,文件的體積也會愈來愈大,若是不加以控制的 話,體積過大的AOF文件極可能對Redis服務器、甚至整個宿主計算機形成影響,而且AOF文 件的體積越大,使用AOF文件來進行數據還原所需的時間就越多。
如 客戶端執行了如下命令是:

> rpush list "A" "B"
OK
> rpush list "C"
OK
> rpush list "D"
OK
> rpush list "E" "F"
OK

  那麼光是爲了記錄這個list鍵的狀態,AOF文件就須要保存四條命令。
  對於實際的應用程度來講,寫命令執行的次數和頻率會比上面的簡單示例要高得多,所 以形成的問題也會嚴重得多。 爲了解決AOF文件體積膨脹的問題,Redis提供了AOF文件重寫(rewrite)功能。經過該 功能,Redis服務器能夠建立一個新的AOF文件來替代現有的AOF文件,新舊兩個AOF文件所 保存的數據庫狀態相同,但新AOF文件不會包含任何浪費空間的冗餘命令,因此新AOF文件 的體積一般會比舊AOF文件的體積要小得多。 在接下來的內容中,咱們將介紹AOF文件重寫的實現原理,以及BGREWEITEAOF命令 的實現原理。
  雖然Redis將生成新AOF文件替換舊AOF文件的功能命名爲「AOF文件重寫」,但實際上, AOF文件重寫並不須要對現有的AOF文件進行任何讀取、分析或者寫入操做,這個功能是通 過讀取服務器當前的數據庫狀態來實現的。
  就像上面的狀況,服務器徹底能夠將這六條命令合併成一條。

> rpush list "A" "B" "C" "D" "E" "F"

  除了上面列舉的列表鍵以外,其餘全部類型的鍵均可以用一樣的方法去減小 AOF文件中的命令數量。首先從數據庫中讀取鍵如今的值,而後用一條命令去記錄鍵值對,代替以前記錄這個鍵值對的多條命令,這就是AOF重寫功能的實現原理。
  在實際中,爲了不在執行命令時形成客戶端輸入緩衝區溢出,重寫程序在處理列表、 哈希表、集合、有序集合這四種可能會帶有多個元素的鍵時,會先檢查鍵所包含的元素數 量,若是元素的數量超過了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那 麼重寫程序將使用多條命令來記錄鍵的值,而不僅僅使用一條命令。 在目前版本中,REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值爲64,這也就是 說,若是一個集合鍵包含了超過64個元素,那麼重寫程序會用多條SADD命令來記錄這個集 合,而且每條命令設置的元素數量也爲64個。

AOF 後臺重寫

  AOF 重寫會執行大量的寫操做,這樣會影響主線程,因此redis AOF 重寫放到了子進程去執行。這樣能夠達到兩個目的:

  • 子進程進行AOF重寫期間,服務器進程(父進程)能夠繼續處理命令請求。
  • 子進程帶有服務器進程的數據副本,使用子進程而不是線程,能夠在避免使用鎖的狀況 下,保證數據的安全性。

可是有一個問題,當子進程重寫數據時,主進程依然在處理新的數據,這也就會形成數據不一致狀況。
爲了解決這種數據不一致問題,Redis服務器設置了一個AOF重寫緩衝區,這個緩衝區在 服務器建立子進程以後開始使用,當Redis服務器執行完一個寫命令以後,它會同時將這個寫 命令發送給AOF緩衝區和AOF重寫緩衝區,以下圖:

這也就是說,在子進程執行AOF重寫期間,服務器進程須要執行如下三個工做:

  1. 執行客戶端發來的命令。
  2. 將執行後的寫命令追加到AOF緩衝區。
  3. 將執行後的寫命令追加到AOF重寫緩衝區。

這樣一來能夠保證:

  • AOF緩衝區的內容會按期被寫入和同步到AOF文件,對現有AOF文件的處理工做會如常 進行。
  • 從建立子進程開始,服務器執行的全部寫命令都會被記錄到AOF重寫緩衝區裏面。

當子進程完成AOF重寫工做以後,它會向父進程發送一個信號,父進程在接到該信號之 後,會調用一個信號處理函數,並執行如下工做:

  1. 將AOF重寫緩衝區中的全部內容寫入到新AOF文件中,這時新AOF文件所保存的數 據庫狀態將和服務器當前的數據庫狀態一致。
  2. 對新的AOF文件進行更名,原子地(atomic)覆蓋現有的AOF文件,完成新舊兩個 AOF文件的替換。

這個信號處理函數執行完畢以後,父進程就能夠繼續像往常同樣接受命令請求了。
在整個AOF後臺重寫過程當中,只有信號處理函數執行時會對服務器進程(父進程)形成 阻塞,在其餘時候,AOF後臺重寫都不會阻塞父進程,這將AOF重寫對服務器性能形成的影 響降到了最低。

Redis 混合持久化

Redis 還能夠同時使用 AOF 持久化和 RDB 持久化。 在這種狀況下, 當 Redis 重啓時, 它會優先使用 AOF 文件來還原數據集, 由於 AOF 文件保存的數據集一般比 RDB 文件所保存的數據集更完整。可是 AOF 恢復比較慢,Redis 4.0 推出了混合持久化

混合持久化: 將 rdb 文件的內容和增量的 AOF 日誌文件存在一塊兒。這裏的 AOF 日誌再也不是全量的日誌,而是 自持久化開始到持久化結束 的這段時間發生的增量 AOF 日誌,一般這部分 AOF 日誌很小。

因而在 Redis 重啓的時候,能夠先加載 RDB 的內容,而後再重放增量 AOF 日誌就能夠徹底替代以前的 AOF 全量文件重放,重啓效率所以大幅獲得提高。

以爲文章不錯的話,小夥伴們麻煩點個贊、關個注、轉個發一下唄~你的支持就是我寫文章的動力。

更多精彩的文章請關注公衆號「蘑菇睡不着」。

你越主動就會越主動,咱們下期見~

相關文章
相關標籤/搜索