redies 持久化方式RDB

RDB

在運行情況下, Redis 以數據結構的形式將數據維持在內存中, 爲了讓這些數據在 Redis 重啓之後仍然可用, Redis 分別提供了 RDB 和 AOF 兩種持久化模式。

在 Redis 運行時, RDB 程序將當前內存中的數據庫快照保存到磁盤文件中, 在 Redis 重啓動時, RDB 程序可以通過載入 RDB 文件來還原數據庫的狀態。

RDB 功能最核心的是 rdbSave 和 rdbLoad 兩個函數, 前者用於生成 RDB 文件到磁盤, 而後者則用於將 RDB 文件中的數據重新載入到內存中:

'digraph persistent {

本章先介紹 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 不能同時執行:

  • 如果 BGSAVE 正在執行,那麼 BGREWRITEAOF 的重寫請求會被延遲到 BGSAVE 執行完畢之後進行,執行 BGREWRITEAOF 命令的客戶端會收到請求被延遲的回覆。
  • 如果 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 命令不能執行。

  • 爲了避免性能問題, BGSAVE 和 BGREWRITEAOF 不能同時執行。

  • 調用 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 文件使用不同的格式來保存不同類型的值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章