在運行情況下,Redis 以數(shù)據(jù)結構的形式將數(shù)據(jù)維持在內存中,為了讓這些數(shù)據(jù)在 Redis 重啟之后仍然可用,Redis 分別提供了 RDB 和 AOF 兩種持久化 模式。
在 Redis 運行時,RDB 程序將當前內存中的數(shù)據(jù)庫快照保存到磁盤文件中,在 Redis 重啟動時,RDB 程序可以通過載入 RDB 文件來還原數(shù)據(jù)庫的狀態(tài)。
RDB 功能最核心的是 rdbSave
和 rdbLoad
兩個函數(shù),前者用于生成 RDB 文件到磁盤,而后者則用于將 RDB 文件中的數(shù)據(jù)重新載入到內存中:
rdb [label = "rdbSave"]; rdb -> redis_object [label = "rdbLoad"];}" />
本章先介紹 SAVE 和 BGSAVE 命令的實現(xiàn),以及 rdbSave
和 rdbLoad
兩個函數(shù)的運行機制,然后以圖表的方式,分部分來介紹 RDB 文件的組織形式。
因為本章涉及 RDB 運行的相關機制,如果還沒了解過 RDB 功能的話,請先閱讀 Redis 官網上的 persistence 手冊 。
rdbSave
函數(shù)負責將內存中的數(shù)據(jù)庫數(shù)據(jù)以 RDB 格式保存到磁盤中,如果 RDB 文件已存在,那么新的 RDB 文件將替換已有的 RDB 文件。
在保存 RDB 文件期間,主進程會被阻塞,直到保存完成為止。
SAVE 和 BGSAVE 兩個命令都會調用 rdbSave
函數(shù),但它們調用的方式各有不同:
rdbSave
,阻塞 Redis 主進程,直到保存完成為止。在主進程阻塞期間,服務器不能處理客戶端的任何請求。fork
出一個子進程,子進程負責調用 rdbSave
,并在保存完成之后向主進程發(fā)送信號,通知保存已完成。因為 rdbSave
在子進程被調用,所以 Redis 服務器在 BGSAVE 執(zhí)行期間仍然可以繼續(xù)處理客戶端的請求。通過偽代碼來描述這兩個命令,可以很容易地看出它們之間的區(qū)別:
def SAVE():
rdbSave()
def BGSAVE():
pid = fork()
if pid == 0:
# 子進程保存 RDB
rdbSave()
elif pid > 0:
# 父進程繼續(xù)處理請求,并等待子進程的完成信號
handle_request()
else:
# pid == -1
# 處理 fork 錯誤
handle_fork_error()
除了了解 RDB 文件的保存方式之外,我們可能還想知道,兩個 RDB 保存命令能否同時使用?它們和 AOF 保存工作是否沖突?
本節(jié)就來解答這些問題。
前面提到過,當 SAVE 執(zhí)行時,Redis 服務器是阻塞的,所以當 SAVE 正在執(zhí)行時,新的 SAVE 、 BGSAVE 或 BGREWRITEAOF 調用都不會產生任何作用。
只有在上一個 SAVE 執(zhí)行完畢、Redis 重新開始接受請求之后,新的 SAVE 、 BGSAVE 或 BGREWRITEAOF 命令才會被處理。
另外,因為 AOF 寫入由后臺線程完成,而 BGREWRITEAOF 則由子進程完成,所以在 SAVE 執(zhí)行的過程中,AOF 寫入和 BGREWRITEAOF 可以同時進行。
在執(zhí)行 SAVE 命令之前,服務器會檢查 BGSAVE 是否正在執(zhí)行當中,如果是的話,服務器就不調用 rdbSave
,而是向客戶端返回一個出錯信息,告知在 BGSAVE 執(zhí)行期間,不能執(zhí)行 SAVE 。
這樣做可以避免 SAVE 和 BGSAVE 調用的兩個 rdbSave
交叉執(zhí)行,造成競爭條件。
另一方面,當 BGSAVE 正在執(zhí)行時,調用新 BGSAVE 命令的客戶端會收到一個出錯信息,告知 BGSAVE 已經在執(zhí)行當中。
BGREWRITEAOF 和 BGSAVE 不能同時執(zhí)行:
BGREWRITEAOF 和 BGSAVE 兩個命令在操作方面并沒有什么沖突的地方,不能同時執(zhí)行它們只是一個性能方面的考慮:并發(fā)出兩個子進程,并且兩個子進程都同時進行大量的磁盤寫入操作,這怎么想都不會是一個好主意。
當 Redis 服務器啟動時,rdbLoad
函數(shù)就會被執(zhí)行,它讀取 RDB 文件,并將文件中的數(shù)據(jù)庫數(shù)據(jù)載入到內存中。
在載入期間,服務器每載入 1000 個鍵就處理一次所有已到達的請求,不過只有 PUBLISH
、 SUBSCRIBE
、 PSUBSCRIBE
、 UNSUBSCRIBE
、 PUNSUBSCRIBE
五個命令的請求會被正確地處理,其他命令一律返回錯誤。等到載入完成之后,服務器才會開始正常處理所有命令。
Note
發(fā)布與訂閱功能和其他數(shù)據(jù)庫功能是完全隔離的,前者不寫入也不讀取數(shù)據(jù)庫,所以在服務器載入期間,訂閱與發(fā)布功能仍然可以正常使用,而不必擔心對載入數(shù)據(jù)的完整性產生影響。
另外,因為 AOF 文件的保存頻率通常要高于 RDB 文件保存的頻率,所以一般來說,AOF 文件中的數(shù)據(jù)會比 RDB 文件中的數(shù)據(jù)要新。
因此,如果服務器在啟動時,打開了 AOF 功能,那么程序優(yōu)先使用 AOF 文件來還原數(shù)據(jù)。只有在 AOF 功能未打開的情況下,Redis 才會使用 RDB 文件來還原數(shù)據(jù)。
前面介紹了保存和讀取 RDB 文件的兩個函數(shù),現(xiàn)在,是時候介紹 RDB 文件本身了。
一個 RDB 文件可以分為以下幾個部分:
+-------+-------------+-----------+-----------------+-----+-----------+
| REDIS | RDB-VERSION | SELECT-DB | KEY-VALUE-PAIRS | EOF | CHECK-SUM |
+-------+-------------+-----------+-----------------+-----+-----------+
|<-------- DB-DATA ---------->|
以下的幾個小節(jié)將分別對這幾個部分的保存和讀入規(guī)則進行介紹。
文件的最開頭保存著 REDIS
五個字符,標識著一個 RDB 文件的開始。
在讀入文件的時候,程序可以通過檢查一個文件的前五個字節(jié),來快速地判斷該文件是否有可能是 RDB 文件。
一個四字節(jié)長的以字符表示的整數(shù),記錄了該文件所使用的 RDB 版本號。
目前的 RDB 文件版本為 0006
。
因為不同版本的 RDB 文件互不兼容,所以在讀入程序時,需要根據(jù)版本來選擇不同的讀入方式。
這個部分在一個 RDB 文件中會出現(xiàn)任意多次,每個 DB-DATA
部分保存著服務器上一個非空數(shù)據(jù)庫的所有數(shù)據(jù)。
這域保存著跟在后面的鍵值對所屬的數(shù)據(jù)庫號碼。
在讀入 RDB 文件時,程序會根據(jù)這個域的值來切換數(shù)據(jù)庫,確保數(shù)據(jù)被還原到正確的數(shù)據(jù)庫上。
因為空的數(shù)據(jù)庫不會被保存到 RDB 文件,所以這個部分至少會包含一個鍵值對的數(shù)據(jù)。
每個鍵值對的數(shù)據(jù)使用以下結構來保存:
+----------------------+---------------+-----+-------+
| OPTIONAL-EXPIRE-TIME | TYPE-OF-VALUE | KEY | VALUE |
+----------------------+---------------+-----+-------+
OPTIONAL-EXPIRE-TIME
域是可選的,如果鍵沒有設置過期時間,那么這個域就不會出現(xiàn);反之,如果這個域出現(xiàn)的話,那么它記錄著鍵的過期時間,在當前版本的 RDB 中,過期時間是一個以毫秒為單位的 UNIX 時間戳。
KEY
域保存著鍵,格式和 REDIS_ENCODING_RAW
編碼的字符串對象一樣(見下文)。
TYPE-OF-VALUE
域記錄著 VALUE
域的值所使用的編碼,根據(jù)這個域的指示,程序會使用不同的方式來保存和讀取 VALUE
的值。
Note
下文提到的編碼在《對象處理機制》章節(jié)介紹過,如果忘記了可以回去重溫下。
保存 VALUE
的詳細格式如下:
REDIS_ENCODING_INT
編碼的 REDIS_STRING
類型對象:
如果值可以表示為 8
位、 16
位或 32
位有符號整數(shù),那么直接以整數(shù)類型的形式來保存它們:
+---------+
| integer |
+---------+
比如說,整數(shù) 8
可以用 8
位序列 00001000
保存。
當讀入這類值時,程序按指定的長度讀入字節(jié)數(shù)據(jù),然后將數(shù)據(jù)轉換回整數(shù)類型。
另一方面,如果值不能被表示為最高 32
位的有符號整數(shù),那么說明這是一個 long long
類型的值,在 RDB 文件中,這種類型的值以字符序列的形式保存。
一個字符序列由兩部分組成:
+-----+---------+
| LEN | CONTENT |
+-----+---------+
其中, CONTENT
域保存了字符內容,而 LEN
則保存了以字節(jié)為單位的字符長度。
當進行載入時,讀入器先讀入 LEN
,創(chuàng)建一個長度等于 LEN
的字符串對象,然后再從文件中讀取 LEN
字節(jié)數(shù)據(jù),并將這些數(shù)據(jù)設置為字符串對象的值。
REDIS_ENCODING_RAW
編碼的 REDIS_STRING
類型值有三種保存方式:
如果值可以表示為 8
位、 16
位或 32
位長的有符號整數(shù),那么用整數(shù)類型的形式來保存它們。
如果字符串長度大于 20
,并且服務器開啟了 LZF 壓縮功能 ,那么對字符串進行壓縮,并保存壓縮之后的數(shù)據(jù)。
經過 LZF 壓縮的字符串會被保存為以下結構:
+----------+----------------+--------------------+
| LZF-FLAG | COMPRESSED-LEN | COMPRESSED-CONTENT |
+----------+----------------+--------------------+
LZF-FLAG
告知讀入器,后面跟著的是被 LZF 算法壓縮過的數(shù)據(jù)。
COMPRESSED-CONTENT
是被壓縮后的數(shù)據(jù), COMPRESSED-LEN
則是該數(shù)據(jù)的字節(jié)長度。
在其他情況下,程序直接以普通字節(jié)序列的方式來保存字符串。比如說,對于一個長度為 20
字節(jié)的字符串,需要使用 20
字節(jié)的空間來保存它。
這種字符串被保存為以下結構:
+-----+---------+
| LEN | CONTENT |
+-----+---------+
LEN
為字符串的字節(jié)長度, CONTENT
為字符串。
當進行載入時,讀入器先檢測字符串保存的方式,再根據(jù)不同的保存方式,用不同的方法取出內容,并將內容保存到新建的字符串對象當中。
REDIS_ENCODING_LINKEDLIST
編碼的 REDIS_LIST
類型值保存為以下結構:
+-----------+--------------+--------------+-----+--------------+
| NODE-SIZE | NODE-VALUE-1 | NODE-VALUE-2 | ... | NODE-VALUE-N |
+-----------+--------------+--------------+-----+--------------+
其中 NODE-SIZE
保存鏈表節(jié)點數(shù)量,后面跟著 NODE-SIZE
個節(jié)點值。節(jié)點值的保存方式和字符串的保存方式一樣。
當進行載入時,讀入器讀取節(jié)點的數(shù)量,創(chuàng)建一個新的鏈表,然后一直執(zhí)行以下步驟,直到指定節(jié)點數(shù)量滿足為止:
REDIS_ENCODING_HT
編碼的 REDIS_SET
類型值保存為以下結構:
+----------+-----------+-----------+-----+-----------+
| SET-SIZE | ELEMENT-1 | ELEMENT-2 | ... | ELEMENT-N |
+----------+-----------+-----------+-----+-----------+
SET-SIZE
記錄了集合元素的數(shù)量,后面跟著多個元素值。元素值的保存方式和字符串的保存方式一樣。
載入時,讀入器先讀入集合元素的數(shù)量 SET-SIZE
,再連續(xù)讀入 SET-SIZE
個字符串,并將這些字符串作為新元素添加至新創(chuàng)建的集合。
REDIS_ENCODING_SKIPLIST
編碼的 REDIS_ZSET
類型值保存為以下結構:
+--------------+-------+---------+-------+---------+-----+-------+---------+
| ELEMENT-SIZE | MEB-1 | SCORE-1 | MEB-2 | SCORE-2 | ... | MEB-N | SCORE-N |
+--------------+-------+---------+-------+---------+-----+-------+---------+
其中 ELEMENT-SIZE
為有序集元素的數(shù)量, MEB-i
為第 i
個有序集元素的成員, SCORE-i
為第 i
個有序集元素的分值。
當進行載入時,讀入器讀取有序集元素數(shù)量,創(chuàng)建一個新的有序集,然后一直執(zhí)行以下步驟,直到指定元素數(shù)量滿足為止:
member
score
,并將它轉換為浮點數(shù)member
為成員、 score
為分值的新元素到有序集REDIS_ENCODING_HT
編碼的 REDIS_HASH
類型值保存為以下結構:
+-----------+-------+---------+-------+---------+-----+-------+---------+
| HASH-SIZE | KEY-1 | VALUE-1 | KEY-2 | VALUE-2 | ... | KEY-N | VALUE-N |
+-----------+-------+---------+-------+---------+-----+-------+---------+
HASH-SIZE
是哈希表包含的鍵值對的數(shù)量, KEY-i
和 VALUE-i
分別是哈希表的鍵和值。
載入時,程序先創(chuàng)建一個新的哈希表,然后讀入 HASH-SIZE
,再執(zhí)行以下步驟 HASH-SIZE
次:
REDIS_LIST
類型、 REDIS_HASH
類型和 REDIS_ZSET
類型都使用了 REDIS_ENCODING_ZIPLIST
編碼, ziplist
在 RDB 中的保存方式如下:
+-----+---------+
| LEN | ZIPLIST |
+-----+---------+
載入時,讀入器先讀入 ziplist
的字節(jié)長,再根據(jù)該字節(jié)長讀入數(shù)據(jù),最后將數(shù)據(jù)還原成一個 ziplist
。
REDIS_ENCODING_INTSET
編碼的 REDIS_SET
類型值保存為以下結構:
+-----+--------+
| LEN | INTSET |
+-----+--------+
載入時,讀入器先讀入 intset
的字節(jié)長度,再根據(jù)長度讀入數(shù)據(jù),最后將數(shù)據(jù)還原成 intset
。
標志著數(shù)據(jù)庫內容的結尾(不是文件的結尾),值為 rdb.h/EDIS_RDB_OPCODE_EOF
(255
)。
RDB 文件所有內容的校驗和,一個 uint_64t
類型值。
REDIS 在寫入 RDB 文件時將校驗和保存在 RDB 文件的末尾,當讀取時,根據(jù)它的值對內容進行校驗。
如果這個域的值為 0
,那么表示 Redis 關閉了校驗和功能。
rdbSave
會將數(shù)據(jù)庫數(shù)據(jù)保存到 RDB 文件,并在保存完成之前阻塞調用者。
SAVE 命令直接調用 rdbSave
,阻塞 Redis 主進程; BGSAVE 用子進程調用 rdbSave
,主進程仍可繼續(xù)處理命令請求。
SAVE 執(zhí)行期間, AOF 寫入可以在后臺線程進行, BGREWRITEAOF 可以在子進程進行,所以這三種操作可以同時進行。
為了避免性能問題, BGSAVE 和 BGREWRITEAOF 不能同時執(zhí)行。
調用 rdbLoad
函數(shù)載入 RDB 文件時,不能進行任何和數(shù)據(jù)庫相關的操作,不過訂閱與發(fā)布方面的命令可以正常執(zhí)行,因為它們和數(shù)據(jù)庫不相關聯(lián)。
RDB 文件的組織方式如下:
+-------+-------------+-----------+-----------------+-----+-----------+
| REDIS | RDB-VERSION | SELECT-DB | KEY-VALUE-PAIRS | EOF | CHECK-SUM |
+-------+-------------+-----------+-----------------+-----+-----------+
|<-------- DB-DATA ---------->|
鍵值對在 RDB 文件中的組織方式如下:
+----------------------+---------------+-----+-------+
| OPTIONAL-EXPIRE-TIME | TYPE-OF-VALUE | KEY | VALUE |
+----------------------+---------------+-----+-------+
RDB 文件使用不同的格式來保存不同類型的值。
更多建議: