RDB

2018-08-02 11:56 更新

RDB

在運(yùn)行情況下,Redis 以數(shù)據(jù)結(jié)構(gòu)的形式將數(shù)據(jù)維持在內(nèi)存中,為了讓這些數(shù)據(jù)在 Redis 重啟之后仍然可用,Redis 分別提供了 RDB 和 AOF 兩種持久化 模式。

在 Redis 運(yùn)行時(shí),RDB 程序?qū)?dāng)前內(nèi)存中的數(shù)據(jù)庫(kù)快照保存到磁盤文件中,在 Redis 重啟動(dòng)時(shí),RDB 程序可以通過(guò)載入 RDB 文件來(lái)還原數(shù)據(jù)庫(kù)的狀態(tài)。

RDB 功能最核心的是 rdbSaverdbLoad 兩個(gè)函數(shù),前者用于生成 RDB 文件到磁盤,而后者則用于將 RDB 文件中的數(shù)據(jù)重新載入到內(nèi)存中:

digraph persistent {    rankdir = LR;    node [shape = circle, style = filled];    edge [style = bold];    redis_object [label =

rdb [label = "rdbSave"]; rdb -> redis_object [label = "rdbLoad"];}" />

本章先介紹 SAVEBGSAVE 命令的實(shí)現(xiàn),以及 rdbSaverdbLoad 兩個(gè)函數(shù)的運(yùn)行機(jī)制,然后以圖表的方式,分部分來(lái)介紹 RDB 文件的組織形式。

因?yàn)楸菊律婕?RDB 運(yùn)行的相關(guān)機(jī)制,如果還沒(méi)了解過(guò) RDB 功能的話,請(qǐng)先閱讀 Redis 官網(wǎng)上的 persistence 手冊(cè) 。

保存

rdbSave 函數(shù)負(fù)責(zé)將內(nèi)存中的數(shù)據(jù)庫(kù)數(shù)據(jù)以 RDB 格式保存到磁盤中,如果 RDB 文件已存在,那么新的 RDB 文件將替換已有的 RDB 文件。

在保存 RDB 文件期間,主進(jìn)程會(huì)被阻塞,直到保存完成為止。

SAVEBGSAVE 兩個(gè)命令都會(huì)調(diào)用 rdbSave 函數(shù),但它們調(diào)用的方式各有不同:

  • SAVE 直接調(diào)用 rdbSave ,阻塞 Redis 主進(jìn)程,直到保存完成為止。在主進(jìn)程阻塞期間,服務(wù)器不能處理客戶端的任何請(qǐng)求。
  • BGSAVEfork 出一個(gè)子進(jìn)程,子進(jìn)程負(fù)責(zé)調(diào)用 rdbSave ,并在保存完成之后向主進(jìn)程發(fā)送信號(hào),通知保存已完成。因?yàn)?rdbSave 在子進(jìn)程被調(diào)用,所以 Redis 服務(wù)器在 BGSAVE 執(zhí)行期間仍然可以繼續(xù)處理客戶端的請(qǐng)求。

通過(guò)偽代碼來(lái)描述這兩個(gè)命令,可以很容易地看出它們之間的區(qū)別:

def SAVE():

    rdbSave()

def BGSAVE():

    pid = fork()

    if pid == 0:

        # 子進(jìn)程保存 RDB
        rdbSave()

    elif pid > 0:

        # 父進(jìn)程繼續(xù)處理請(qǐng)求,并等待子進(jìn)程的完成信號(hào)
        handle_request()

    else:

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

SAVE 、 BGSAVE 、 AOF 寫(xiě)入和 BGREWRITEAOF

除了了解 RDB 文件的保存方式之外,我們可能還想知道,兩個(gè) RDB 保存命令能否同時(shí)使用?它們和 AOF 保存工作是否沖突?

本節(jié)就來(lái)解答這些問(wèn)題。

SAVE

前面提到過(guò),當(dāng) SAVE 執(zhí)行時(shí),Redis 服務(wù)器是阻塞的,所以當(dāng) SAVE 正在執(zhí)行時(shí),新的 SAVE 、 BGSAVEBGREWRITEAOF 調(diào)用都不會(huì)產(chǎn)生任何作用。

只有在上一個(gè) SAVE 執(zhí)行完畢、Redis 重新開(kāi)始接受請(qǐng)求之后,新的 SAVE 、 BGSAVEBGREWRITEAOF 命令才會(huì)被處理。

另外,因?yàn)?AOF 寫(xiě)入由后臺(tái)線程完成,而 BGREWRITEAOF 則由子進(jìn)程完成,所以在 SAVE 執(zhí)行的過(guò)程中,AOF 寫(xiě)入和 BGREWRITEAOF 可以同時(shí)進(jìn)行。

BGSAVE

在執(zhí)行 SAVE 命令之前,服務(wù)器會(huì)檢查 BGSAVE 是否正在執(zhí)行當(dāng)中,如果是的話,服務(wù)器就不調(diào)用 rdbSave ,而是向客戶端返回一個(gè)出錯(cuò)信息,告知在 BGSAVE 執(zhí)行期間,不能執(zhí)行 SAVE 。

這樣做可以避免 SAVEBGSAVE 調(diào)用的兩個(gè) rdbSave 交叉執(zhí)行,造成競(jìng)爭(zhēng)條件。

另一方面,當(dāng) BGSAVE 正在執(zhí)行時(shí),調(diào)用新 BGSAVE 命令的客戶端會(huì)收到一個(gè)出錯(cuò)信息,告知 BGSAVE 已經(jīng)在執(zhí)行當(dāng)中。

BGREWRITEAOFBGSAVE 不能同時(shí)執(zhí)行:

  • 如果 BGSAVE 正在執(zhí)行,那么 BGREWRITEAOF 的重寫(xiě)請(qǐng)求會(huì)被延遲到 BGSAVE 執(zhí)行完畢之后進(jìn)行,執(zhí)行 BGREWRITEAOF 命令的客戶端會(huì)收到請(qǐng)求被延遲的回復(fù)。
  • 如果 BGREWRITEAOF 正在執(zhí)行,那么調(diào)用 BGSAVE 的客戶端將收到出錯(cuò)信息,表示這兩個(gè)命令不能同時(shí)執(zhí)行。

BGREWRITEAOFBGSAVE 兩個(gè)命令在操作方面并沒(méi)有什么沖突的地方,不能同時(shí)執(zhí)行它們只是一個(gè)性能方面的考慮:并發(fā)出兩個(gè)子進(jìn)程,并且兩個(gè)子進(jìn)程都同時(shí)進(jìn)行大量的磁盤寫(xiě)入操作,這怎么想都不會(huì)是一個(gè)好主意。

載入

當(dāng) Redis 服務(wù)器啟動(dòng)時(shí),rdbLoad 函數(shù)就會(huì)被執(zhí)行,它讀取 RDB 文件,并將文件中的數(shù)據(jù)庫(kù)數(shù)據(jù)載入到內(nèi)存中。

在載入期間,服務(wù)器每載入 1000 個(gè)鍵就處理一次所有已到達(dá)的請(qǐng)求,不過(guò)只有 PUBLISHSUBSCRIBE 、 PSUBSCRIBEUNSUBSCRIBE 、 PUNSUBSCRIBE 五個(gè)命令的請(qǐng)求會(huì)被正確地處理,其他命令一律返回錯(cuò)誤。等到載入完成之后,服務(wù)器才會(huì)開(kāi)始正常處理所有命令。

Note

發(fā)布與訂閱功能和其他數(shù)據(jù)庫(kù)功能是完全隔離的,前者不寫(xiě)入也不讀取數(shù)據(jù)庫(kù),所以在服務(wù)器載入期間,訂閱與發(fā)布功能仍然可以正常使用,而不必?fù)?dān)心對(duì)載入數(shù)據(jù)的完整性產(chǎn)生影響。

另外,因?yàn)?AOF 文件的保存頻率通常要高于 RDB 文件保存的頻率,所以一般來(lái)說(shuō),AOF 文件中的數(shù)據(jù)會(huì)比 RDB 文件中的數(shù)據(jù)要新。

因此,如果服務(wù)器在啟動(dòng)時(shí),打開(kāi)了 AOF 功能,那么程序優(yōu)先使用 AOF 文件來(lái)還原數(shù)據(jù)。只有在 AOF 功能未打開(kāi)的情況下,Redis 才會(huì)使用 RDB 文件來(lái)還原數(shù)據(jù)。

RDB 文件結(jié)構(gòu)

前面介紹了保存和讀取 RDB 文件的兩個(gè)函數(shù),現(xiàn)在,是時(shí)候介紹 RDB 文件本身了。

一個(gè) RDB 文件可以分為以下幾個(gè)部分:

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

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

以下的幾個(gè)小節(jié)將分別對(duì)這幾個(gè)部分的保存和讀入規(guī)則進(jìn)行介紹。

REDIS

文件的最開(kāi)頭保存著 REDIS 五個(gè)字符,標(biāo)識(shí)著一個(gè) RDB 文件的開(kāi)始。

在讀入文件的時(shí)候,程序可以通過(guò)檢查一個(gè)文件的前五個(gè)字節(jié),來(lái)快速地判斷該文件是否有可能是 RDB 文件。

RDB-VERSION

一個(gè)四字節(jié)長(zhǎng)的以字符表示的整數(shù),記錄了該文件所使用的 RDB 版本號(hào)。

目前的 RDB 文件版本為 0006 。

因?yàn)椴煌姹镜?RDB 文件互不兼容,所以在讀入程序時(shí),需要根據(jù)版本來(lái)選擇不同的讀入方式。

DB-DATA

這個(gè)部分在一個(gè) RDB 文件中會(huì)出現(xiàn)任意多次,每個(gè) DB-DATA 部分保存著服務(wù)器上一個(gè)非空數(shù)據(jù)庫(kù)的所有數(shù)據(jù)。

SELECT-DB

這域保存著跟在后面的鍵值對(duì)所屬的數(shù)據(jù)庫(kù)號(hào)碼。

在讀入 RDB 文件時(shí),程序會(huì)根據(jù)這個(gè)域的值來(lái)切換數(shù)據(jù)庫(kù),確保數(shù)據(jù)被還原到正確的數(shù)據(jù)庫(kù)上。

KEY-VALUE-PAIRS

因?yàn)榭盏臄?shù)據(jù)庫(kù)不會(huì)被保存到 RDB 文件,所以這個(gè)部分至少會(huì)包含一個(gè)鍵值對(duì)的數(shù)據(jù)。

每個(gè)鍵值對(duì)的數(shù)據(jù)使用以下結(jié)構(gòu)來(lái)保存:

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

OPTIONAL-EXPIRE-TIME 域是可選的,如果鍵沒(méi)有設(shè)置過(guò)期時(shí)間,那么這個(gè)域就不會(huì)出現(xiàn);反之,如果這個(gè)域出現(xiàn)的話,那么它記錄著鍵的過(guò)期時(shí)間,在當(dāng)前版本的 RDB 中,過(guò)期時(shí)間是一個(gè)以毫秒為單位的 UNIX 時(shí)間戳。

KEY 域保存著鍵,格式和 REDIS_ENCODING_RAW 編碼的字符串對(duì)象一樣(見(jiàn)下文)。

TYPE-OF-VALUE 域記錄著 VALUE 域的值所使用的編碼,根據(jù)這個(gè)域的指示,程序會(huì)使用不同的方式來(lái)保存和讀取 VALUE 的值。

Note

下文提到的編碼在《對(duì)象處理機(jī)制》章節(jié)介紹過(guò),如果忘記了可以回去重溫下。

保存 VALUE 的詳細(xì)格式如下:

  • REDIS_ENCODING_INT 編碼的 REDIS_STRING 類型對(duì)象:

如果值可以表示為 8 位、 16 位或 32 位有符號(hào)整數(shù),那么直接以整數(shù)類型的形式來(lái)保存它們:

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

比如說(shuō),整數(shù) 8 可以用 8 位序列 00001000 保存。

當(dāng)讀入這類值時(shí),程序按指定的長(zhǎng)度讀入字節(jié)數(shù)據(jù),然后將數(shù)據(jù)轉(zhuǎn)換回整數(shù)類型。

另一方面,如果值不能被表示為最高 32 位的有符號(hào)整數(shù),那么說(shuō)明這是一個(gè) long long 類型的值,在 RDB 文件中,這種類型的值以字符序列的形式保存。

一個(gè)字符序列由兩部分組成:

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

其中, CONTENT 域保存了字符內(nèi)容,而 LEN 則保存了以字節(jié)為單位的字符長(zhǎng)度。

當(dāng)進(jìn)行載入時(shí),讀入器先讀入 LEN ,創(chuàng)建一個(gè)長(zhǎng)度等于 LEN 的字符串對(duì)象,然后再?gòu)奈募凶x取 LEN 字節(jié)數(shù)據(jù),并將這些數(shù)據(jù)設(shè)置為字符串對(duì)象的值。

  • REDIS_ENCODING_RAW 編碼的 REDIS_STRING 類型值有三種保存方式:

  1. 如果值可以表示為 8 位、 16 位或 32 位長(zhǎng)的有符號(hào)整數(shù),那么用整數(shù)類型的形式來(lái)保存它們。

  2. 如果字符串長(zhǎng)度大于 20 ,并且服務(wù)器開(kāi)啟了 LZF 壓縮功能 ,那么對(duì)字符串進(jìn)行壓縮,并保存壓縮之后的數(shù)據(jù)。

經(jīng)過(guò) LZF 壓縮的字符串會(huì)被保存為以下結(jié)構(gòu):

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

LZF-FLAG 告知讀入器,后面跟著的是被 LZF 算法壓縮過(guò)的數(shù)據(jù)。

COMPRESSED-CONTENT 是被壓縮后的數(shù)據(jù), COMPRESSED-LEN 則是該數(shù)據(jù)的字節(jié)長(zhǎng)度。

  1. 在其他情況下,程序直接以普通字節(jié)序列的方式來(lái)保存字符串。比如說(shuō),對(duì)于一個(gè)長(zhǎng)度為 20 字節(jié)的字符串,需要使用 20 字節(jié)的空間來(lái)保存它。

這種字符串被保存為以下結(jié)構(gòu):

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

LEN 為字符串的字節(jié)長(zhǎng)度, CONTENT 為字符串。

當(dāng)進(jìn)行載入時(shí),讀入器先檢測(cè)字符串保存的方式,再根據(jù)不同的保存方式,用不同的方法取出內(nèi)容,并將內(nèi)容保存到新建的字符串對(duì)象當(dāng)中。

  • REDIS_ENCODING_LINKEDLIST 編碼的 REDIS_LIST 類型值保存為以下結(jié)構(gòu):

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

其中 NODE-SIZE 保存鏈表節(jié)點(diǎn)數(shù)量,后面跟著 NODE-SIZE 個(gè)節(jié)點(diǎn)值。節(jié)點(diǎn)值的保存方式和字符串的保存方式一樣。

當(dāng)進(jìn)行載入時(shí),讀入器讀取節(jié)點(diǎn)的數(shù)量,創(chuàng)建一個(gè)新的鏈表,然后一直執(zhí)行以下步驟,直到指定節(jié)點(diǎn)數(shù)量滿足為止:

  1. 讀取字符串表示的節(jié)點(diǎn)值
  2. 將包含節(jié)點(diǎn)值的新節(jié)點(diǎn)添加到鏈表中
  • REDIS_ENCODING_HT 編碼的 REDIS_SET 類型值保存為以下結(jié)構(gòu):

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

SET-SIZE 記錄了集合元素的數(shù)量,后面跟著多個(gè)元素值。元素值的保存方式和字符串的保存方式一樣。

載入時(shí),讀入器先讀入集合元素的數(shù)量 SET-SIZE ,再連續(xù)讀入 SET-SIZE 個(gè)字符串,并將這些字符串作為新元素添加至新創(chuàng)建的集合。

  • REDIS_ENCODING_SKIPLIST 編碼的 REDIS_ZSET 類型值保存為以下結(jié)構(gòu):

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

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

當(dāng)進(jìn)行載入時(shí),讀入器讀取有序集元素?cái)?shù)量,創(chuàng)建一個(gè)新的有序集,然后一直執(zhí)行以下步驟,直到指定元素?cái)?shù)量滿足為止:

  1. 讀入字符串形式保存的成員 member
  2. 讀入字符串形式保存的分值 score ,并將它轉(zhuǎn)換為浮點(diǎn)數(shù)
  3. 添加 member 為成員、 score 為分值的新元素到有序集
  • REDIS_ENCODING_HT 編碼的 REDIS_HASH 類型值保存為以下結(jié)構(gòu):

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

HASH-SIZE 是哈希表包含的鍵值對(duì)的數(shù)量, KEY-iVALUE-i 分別是哈希表的鍵和值。

載入時(shí),程序先創(chuàng)建一個(gè)新的哈希表,然后讀入 HASH-SIZE ,再執(zhí)行以下步驟 HASH-SIZE 次:

  1. 讀入一個(gè)字符串
  2. 再讀入另一個(gè)字符串
  3. 將第一個(gè)讀入的字符串作為鍵,第二個(gè)讀入的字符串作為值,插入到新建立的哈希中。
  • REDIS_LIST 類型、 REDIS_HASH 類型和 REDIS_ZSET 類型都使用了 REDIS_ENCODING_ZIPLIST 編碼, ziplist 在 RDB 中的保存方式如下:

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

載入時(shí),讀入器先讀入 ziplist 的字節(jié)長(zhǎng),再根據(jù)該字節(jié)長(zhǎng)讀入數(shù)據(jù),最后將數(shù)據(jù)還原成一個(gè) ziplist 。

  • REDIS_ENCODING_INTSET 編碼的 REDIS_SET 類型值保存為以下結(jié)構(gòu):

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

載入時(shí),讀入器先讀入 intset 的字節(jié)長(zhǎng)度,再根據(jù)長(zhǎng)度讀入數(shù)據(jù),最后將數(shù)據(jù)還原成 intset 。

EOF

標(biāo)志著數(shù)據(jù)庫(kù)內(nèi)容的結(jié)尾(不是文件的結(jié)尾),值為 rdb.h/EDIS_RDB_OPCODE_EOF255)。

CHECK-SUM

RDB 文件所有內(nèi)容的校驗(yàn)和,一個(gè) uint_64t 類型值。

REDIS 在寫(xiě)入 RDB 文件時(shí)將校驗(yàn)和保存在 RDB 文件的末尾,當(dāng)讀取時(shí),根據(jù)它的值對(duì)內(nèi)容進(jìn)行校驗(yàn)。

如果這個(gè)域的值為 0 ,那么表示 Redis 關(guān)閉了校驗(yàn)和功能。

小結(jié)

  • rdbSave 會(huì)將數(shù)據(jù)庫(kù)數(shù)據(jù)保存到 RDB 文件,并在保存完成之前阻塞調(diào)用者。

  • SAVE 命令直接調(diào)用 rdbSave ,阻塞 Redis 主進(jìn)程; BGSAVE 用子進(jìn)程調(diào)用 rdbSave ,主進(jìn)程仍可繼續(xù)處理命令請(qǐng)求。

  • SAVE 執(zhí)行期間, AOF 寫(xiě)入可以在后臺(tái)線程進(jìn)行, BGREWRITEAOF 可以在子進(jìn)程進(jìn)行,所以這三種操作可以同時(shí)進(jìn)行。

  • 為了避免產(chǎn)生競(jìng)爭(zhēng)條件, BGSAVE 執(zhí)行時(shí), SAVE 命令不能執(zhí)行。

  • 為了避免性能問(wèn)題, BGSAVEBGREWRITEAOF 不能同時(shí)執(zhí)行。

  • 調(diào)用 rdbLoad 函數(shù)載入 RDB 文件時(shí),不能進(jìn)行任何和數(shù)據(jù)庫(kù)相關(guān)的操作,不過(guò)訂閱與發(fā)布方面的命令可以正常執(zhí)行,因?yàn)樗鼈兒蛿?shù)據(jù)庫(kù)不相關(guān)聯(lián)。

  • RDB 文件的組織方式如下:

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

                      |<-------- DB-DATA ---------->|
  • 鍵值對(duì)在 RDB 文件中的組織方式如下:

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

RDB 文件使用不同的格式來(lái)保存不同類型的值。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)