1. 程式人生 > >Redis 設計與實現之RDB 和 AOF 兩種持久化模式詳解

Redis 設計與實現之RDB 和 AOF 兩種持久化模式詳解

在執行情況下, Redis 以資料結構的形式將資料維持在記憶體中, 為了讓這些資料在 Redis 重啟之後仍然可用, Redis 分別提供了 RDB 和 AOF 兩種持久化模式。

在 Redis 執行時, RDB 程式將當前記憶體中的資料庫快照儲存到磁碟檔案中, 在 Redis 重啟動時, RDB 程式可以通過載入 RDB 檔案來還原資料庫的狀態。

RDB 功能最核心的是 rdbSave 和 rdbLoad 兩個函式, 前者用於生成 RDB 檔案到磁碟, 而後者則用於將 RDB 檔案中的資料重新載入到記憶體中:

digraph persistent {      rankdir = LR;      node [shape = circle, style = filled];      edge [style = bold];      redis_object [label = "記憶體中的\n資料物件", fillcolor = "#A8E270"];      rdb [label = "磁碟中的\nRDB檔案", fillcolor = "#95BBE3"];      redis_object -> rdb [label = "rdbSave"];      rdb -> redis_object [label = "rdbLoad"]; }

本章先介紹 SAVE 和 BGSAVE 命令的實現, 以及 rdbSave

 和 rdbLoad 兩個函式的執行機制, 然後以圖表的方式, 分部分來介紹 RDB 檔案的組織形式。

因為本章涉及 RDB 執行的相關機制, 如果還沒了解過 RDB 功能的話, 請先閱讀 Redis 官網上的 persistence 手冊 。

儲存

rdbSave 函式負責將記憶體中的資料庫資料以 RDB 格式儲存到磁碟中, 如果 RDB 檔案已存在, 那麼新的 RDB 檔案將替換已有的 RDB 檔案。

在儲存 RDB 檔案期間, 主程序會被阻塞, 直到儲存完成為止。

SAVE 和 BGSAVE 兩個命令都會呼叫 rdbSave 函式,但它們呼叫的方式各有不同:

  • SAVE 直接呼叫 rdbSave
     ,阻塞 Redis 主程序,直到儲存完成為止。在主程序阻塞期間,伺服器不能處理客戶端的任何請求。
  • BGSAVE 則 fork 出一個子程序,子程序負責呼叫 rdbSave ,並在儲存完成之後向主程序傳送訊號,通知儲存已完成。因為 rdbSave 在子程序被呼叫,所以 Redis 伺服器在 BGSAVE 執行期間仍然可以繼續處理客戶端的請求。

通過虛擬碼來描述這兩個命令,可以很容易地看出它們之間的區別:

def SAVE():

    rdbSave()


def BGSAVE():

    pid = fork()

    if pid == 0:

        # 子程序儲存 RDB
        rdbSave()

    elif pid > 0:

        # 父程序繼續處理請求,並等待子程序的完成訊號
        handle_request()

    else:

        # pid == -1
        # 處理 fork 錯誤
        handle_fork_error()

SAVE 、 BGSAVE 、 AOF 寫入和 BGREWRITEAOF

除了瞭解 RDB 檔案的儲存方式之外, 我們可能還想知道, 兩個 RDB 儲存命令能否同時使用? 它們和 AOF 儲存工作是否衝突?

本節就來解答這些問題。

SAVE

前面提到過, 當 SAVE 執行時, Redis 伺服器是阻塞的, 所以當 SAVE 正在執行時, 新的 SAVE 、 BGSAVE 或 BGREWRITEAOF 呼叫都不會產生任何作用。

只有在上一個 SAVE 執行完畢、 Redis 重新開始接受請求之後, 新的 SAVE 、 BGSAVE 或 BGREWRITEAOF 命令才會被處理。

另外, 因為 AOF 寫入由後臺執行緒完成, 而 BGREWRITEAOF 則由子程序完成, 所以在 SAVE 執行的過程中, AOF 寫入和 BGREWRITEAOF 可以同時進行。

BGSAVE

在執行 SAVE 命令之前, 伺服器會檢查 BGSAVE 是否正在執行當中, 如果是的話, 伺服器就不呼叫 rdbSave , 而是向客戶端返回一個出錯資訊, 告知在 BGSAVE 執行期間, 不能執行 SAVE 。

這樣做可以避免 SAVE 和 BGSAVE 呼叫的兩個 rdbSave 交叉執行, 造成競爭條件。

另一方面, 當 BGSAVE 正在執行時, 呼叫新 BGSAVE 命令的客戶端會收到一個出錯資訊, 告知 BGSAVE 已經在執行當中。

  • 如果 BGREWRITEAOF 正在執行,那麼呼叫 BGSAVE 的客戶端將收到出錯資訊,表示這兩個命令不能同時執行。

BGREWRITEAOF 和 BGSAVE 兩個命令在操作方面並沒有什麼衝突的地方, 不能同時執行它們只是一個性能方面的考慮: 併發出兩個子程序, 並且兩個子程序都同時進行大量的磁碟寫入操作, 這怎麼想都不會是一個好主意。

載入

當 Redis 伺服器啟動時, rdbLoad 函式就會被執行, 它讀取 RDB 檔案, 並將檔案中的資料庫資料載入到記憶體中。

在載入期間, 伺服器每載入 1000 個鍵就處理一次所有已到達的請求, 不過只有 PUBLISH 、 SUBSCRIBE 、 PSUBSCRIBE 、 UNSUBSCRIBE 、 PUNSUBSCRIBE 五個命令的請求會被正確地處理, 其他命令一律返回錯誤。 等到載入完成之後, 伺服器才會開始正常處理所有命令。

釋出與訂閱功能和其他資料庫功能是完全隔離的,前者不寫入也不讀取資料庫,所以在伺服器載入期間,訂閱與釋出功能仍然可以正常使用,而不必擔心對載入資料的完整性產生影響。

另外, 因為 AOF 檔案的儲存頻率通常要高於 RDB 檔案儲存的頻率, 所以一般來說, AOF 檔案中的資料會比 RDB 檔案中的資料要新。

因此, 如果伺服器在啟動時, 打開了 AOF 功能, 那麼程式優先使用 AOF 檔案來還原資料。 只有在 AOF 功能未開啟的情況下, Redis 才會使用 RDB 檔案來還原資料。

RDB 檔案結構

前面介紹了儲存和讀取 RDB 檔案的兩個函式,現在,是時候介紹 RDB 檔案本身了。

一個 RDB 檔案可以分為以下幾個部分:

+-------+-------------+-----------+-----------------+-----+-----------+
| REDIS | RDB-VERSION | SELECT-DB | KEY-VALUE-PAIRS | EOF | CHECK-SUM |
+-------+-------------+-----------+-----------------+-----+-----------+

                      |<-------- DB-DATA ---------->|

以下的幾個小節將分別對這幾個部分的儲存和讀入規則進行介紹。

REDIS

檔案的最開頭儲存著 REDIS 五個字元,標識著一個 RDB 檔案的開始。

在讀入檔案的時候,程式可以通過檢查一個檔案的前五個位元組,來快速地判斷該檔案是否有可能是 RDB 檔案。

RDB-VERSION

一個四位元組長的以字元表示的整數,記錄了該檔案所使用的 RDB 版本號。

目前的 RDB 檔案版本為 0006 。

因為不同版本的 RDB 檔案互不相容,所以在讀入程式時,需要根據版本來選擇不同的讀入方式。

DB-DATA

這個部分在一個 RDB 檔案中會出現任意多次,每個 DB-DATA 部分儲存著伺服器上一個非空資料庫的所有資料。

SELECT-DB

這域儲存著跟在後面的鍵值對所屬的資料庫號碼。

在讀入 RDB 檔案時,程式會根據這個域的值來切換資料庫,確保資料被還原到正確的資料庫上。

KEY-VALUE-PAIRS

因為空的資料庫不會被儲存到 RDB 檔案,所以這個部分至少會包含一個鍵值對的資料。

每個鍵值對的資料使用以下結構來儲存:

+----------------------+---------------+-----+-------+
| OPTIONAL-EXPIRE-TIME | TYPE-OF-VALUE | KEY | VALUE |
+----------------------+---------------+-----+-------+

OPTIONAL-EXPIRE-TIME 域是可選的,如果鍵沒有設定過期時間,那麼這個域就不會出現; 反之,如果這個域出現的話,那麼它記錄著鍵的過期時間,在當前版本的 RDB 中,過期時間是一個以毫秒為單位的 UNIX 時間戳。

KEY 域儲存著鍵,格式和 REDIS_ENCODING_RAW 編碼的字串物件一樣(見下文)。

TYPE-OF-VALUE 域記錄著 VALUE 域的值所使用的編碼, 根據這個域的指示, 程式會使用不同的方式來儲存和讀取 VALUE 的值。

下文提到的編碼在《物件處理機制》章節介紹過,如果忘記了可以回去重溫下。

儲存 VALUE 的詳細格式如下:

  • REDIS_ENCODING_INT 編碼的 REDIS_STRING 型別物件:

    如果值可以表示為 8 位、 16 位或 32 位有符號整數,那麼直接以整數型別的形式來儲存它們:

    +---------+
    | integer |
    +---------+
    

    比如說,整數 8 可以用 8 位序列 00001000 儲存。

    當讀入這類值時,程式按指定的長度讀入位元組資料,然後將資料轉換回整數型別。

    另一方面,如果值不能被表示為最高 32 位的有符號整數,那麼說明這是一個 long long 型別的值,在 RDB 檔案中,這種型別的值以字元序列的形式儲存。

    一個字元序列由兩部分組成:

    +-----+---------+
    | LEN | CONTENT |
    +-----+---------+
    

    其中, CONTENT 域儲存了字元內容,而 LEN 則儲存了以位元組為單位的字元長度。

    當進行載入時,讀入器先讀入 LEN ,建立一個長度等於 LEN 的字串物件,然後再從檔案中讀取 LEN 位元組資料,並將這些資料設定為字串物件的值。

  • REDIS_ENCODING_RAW 編碼的 REDIS_STRING 型別值有三種儲存方式:

    1. 如果值可以表示為 8 位、 16 位或 32 位長的有符號整數,那麼用整數型別的形式來儲存它們。

    2. 如果字串長度大於 20 ,並且伺服器開啟了 LZF 壓縮功能 ,那麼對字串進行壓縮,並儲存壓縮之後的資料。

      經過 LZF 壓縮的字串會被儲存為以下結構:

      +----------+----------------+--------------------+
      | LZF-FLAG | COMPRESSED-LEN | COMPRESSED-CONTENT |
      +----------+----------------+--------------------+
      

      LZF-FLAG 告知讀入器,後面跟著的是被 LZF 演算法壓縮過的資料。

      COMPRESSED-CONTENT 是被壓縮後的資料, COMPRESSED-LEN 則是該資料的位元組長度。

    3. 在其他情況下,程式直接以普通位元組序列的方式來儲存字串。比如說,對於一個長度為 20 位元組的字串,需要使用 20 位元組的空間來儲存它。

      這種字串被儲存為以下結構:

      +-----+---------+
      | LEN | CONTENT |
      +-----+---------+
      

      LEN 為字串的位元組長度, CONTENT 為字串。

    當進行載入時,讀入器先檢測字串儲存的方式,再根據不同的儲存方式,用不同的方法取出內容,並將內容儲存到新建的字串物件當中。

  • REDIS_ENCODING_LINKEDLIST 編碼的 REDIS_LIST 型別值儲存為以下結構:

    +-----------+--------------+--------------+-----+--------------+
    | NODE-SIZE | NODE-VALUE-1 | NODE-VALUE-2 | ... | NODE-VALUE-N |
    +-----------+--------------+--------------+-----+--------------+
    

    其中 NODE-SIZE 儲存連結串列節點數量,後面跟著 NODE-SIZE 個節點值。節點值的儲存方式和字串的儲存方式一樣。

    當進行載入時,讀入器讀取節點的數量,建立一個新的連結串列,然後一直執行以下步驟,直到指定節點數量滿足為止:

    1. 讀取字串表示的節點值
    2. 將包含節點值的新節點新增到連結串列中
  • REDIS_ENCODING_HT 編碼的 REDIS_SET 型別值儲存為以下結構:

    +----------+-----------+-----------+-----+-----------+
    | SET-SIZE | ELEMENT-1 | ELEMENT-2 | ... | ELEMENT-N |
    +----------+-----------+-----------+-----+-----------+
    

    SET-SIZE 記錄了集合元素的數量,後面跟著多個元素值。元素值的儲存方式和字串的儲存方式一樣。

    載入時,讀入器先讀入集合元素的數量 SET-SIZE ,再連續讀入 SET-SIZE 個字串,並將這些字串作為新元素新增至新建立的集合。

  • REDIS_ENCODING_SKIPLIST 編碼的 REDIS_ZSET 型別值儲存為以下結構:

    +--------------+-------+---------+-------+---------+-----+-------+---------+
    | ELEMENT-SIZE | MEB-1 | SCORE-1 | MEB-2 | SCORE-2 | ... | MEB-N | SCORE-N |
    +--------------+-------+---------+-------+---------+-----+-------+---------+
    

    其中 ELEMENT-SIZE 為有序集元素的數量, MEB-i 為第 i 個有序集元素的成員, SCORE-i 為第 i 個有序集元素的分值。

    當進行載入時,讀入器讀取有序集元素數量,建立一個新的有序集,然後一直執行以下步驟,直到指定元素數量滿足為止:

    1. 讀入字串形式儲存的成員 member
    2. 讀入字串形式儲存的分值 score ,並將它轉換為浮點數
    3. 新增 member 為成員、 score 為分值的新元素到有序集
  • REDIS_ENCODING_HT 編碼的 REDIS_HASH 型別值儲存為以下結構:

    +-----------+-------+---------+-------+---------+-----+-------+---------+
    | HASH-SIZE | KEY-1 | VALUE-1 | KEY-2 | VALUE-2 | ... | KEY-N | VALUE-N |
    +-----------+-------+---------+-------+---------+-----+-------+---------+
    

    HASH-SIZE 是雜湊表包含的鍵值對的數量, KEY-i 和 VALUE-i 分別是雜湊表的鍵和值。

    載入時,程式先建立一個新的雜湊表,然後讀入 HASH-SIZE ,再執行以下步驟 HASH-SIZE 次:

    1. 讀入一個字串
    2. 再讀入另一個字串
    3. 將第一個讀入的字串作為鍵,第二個讀入的字串作為值,插入到新建立的雜湊中。
  • REDIS_LIST 型別、 REDIS_HASH 型別和 REDIS_ZSET 型別都使用了 REDIS_ENCODING_ZIPLIST 編碼, ziplist 在 RDB 中的儲存方式如下:

    +-----+---------+
    | LEN | ZIPLIST |
    +-----+---------+
    

    載入時,讀入器先讀入 ziplist 的位元組長,再根據該位元組長讀入資料,最後將資料還原成一個 ziplist 。

  • REDIS_ENCODING_INTSET 編碼的 REDIS_SET 型別值儲存為以下結構:

    +-----+--------+
    | LEN | INTSET |
    +-----+--------+
    

    載入時,讀入器先讀入 intset 的位元組長度,再根據長度讀入資料,最後將資料還原成 intset 。

EOF

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

CHECK-SUM

RDB 檔案所有內容的校驗和, 一個 uint_64t 型別值。

REDIS 在寫入 RDB 檔案時將校驗和儲存在 RDB 檔案的末尾, 當讀取時, 根據它的值對內容進行校驗。

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

小結

  • rdbSave 會將資料庫資料儲存到 RDB 檔案,並在儲存完成之前阻塞呼叫者。

  • SAVE 命令直接呼叫 rdbSave ,阻塞 Redis 主程序; BGSAVE 用子程序呼叫 rdbSave ,主程序仍可繼續處理命令請求。

  • SAVE 執行期間, AOF 寫入可以在後臺執行緒進行, BGREWRITEAOF 可以在子程序進行,所以這三種操作可以同時進行。

  • 為了避免產生競爭條件, BGSAVE 執行時, SAVE 命令不能執行。

  • 呼叫 rdbLoad 函式載入 RDB 檔案時,不能進行任何和資料庫相關的操作,不過訂閱與釋出方面的命令可以正常執行,因為它們和資料庫不相關聯。

  • RDB 檔案的組織方式如下:

    +-------+-------------+-----------+-----------------+-----+-----------+
    | REDIS | RDB-VERSION | SELECT-DB | KEY-VALUE-PAIRS | EOF | CHECK-SUM |
    +-------+-------------+-----------+-----------------+-----+-----------+
    
                          |<-------- DB-DATA ---------->|
    
  • 鍵值對在 RDB 檔案中的組織方式如下:

    +----------------------+---------------+-----+-------+
    | OPTIONAL-EXPIRE-TIME | TYPE-OF-VALUE | KEY | VALUE |
    +----------------------+---------------+-----+-------+
    

    RDB 檔案使用不同的格式來儲存不同型別的值。

轉載自