RDB
在運行情況下, Redis 以數據結構的形式將數據維持在內存中, 爲了讓這些數據在 Redis 重啓之後仍然可用, Redis 分別提供了 RDB 和 AOF 兩種持久化模式。
在 Redis 運行時, RDB 程序將當前內存中的數據庫快照保存到磁盤文件中, 在 Redis 重啓動時, RDB 程序可以通過載入 RDB 文件來還原數據庫的狀態。
RDB 功能最核心的是 rdbSave
和 rdbLoad
兩個函數, 前者用於生成 RDB 文件到磁盤, 而後者則用於將 RDB 文件中的數據重新載入到內存中:
本章先介紹 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 ---------->|
以下的幾個小節將分別對這幾個部分的保存和讀入規則進行介紹。
RDB-VERSION
一個四字節長的以字符表示的整數,記錄了該文件所使用的 RDB 版本號。
目前的 RDB 文件版本爲 0006
。
因爲不同版本的 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
類型值有三種保存方式:如果值可以表示爲
8
位、16
位或32
位長的有符號整數,那麼用整數類型的形式來保存它們。如果字符串長度大於
20
,並且服務器開啓了 LZF 壓縮功能 ,那麼對字符串進行壓縮,並保存壓縮之後的數據。經過 LZF 壓縮的字符串會被保存爲以下結構:
+----------+----------------+--------------------+ | LZF-FLAG | COMPRESSED-LEN | COMPRESSED-CONTENT | +----------+----------------+--------------------+
LZF-FLAG
告知讀入器,後面跟着的是被 LZF 算法壓縮過的數據。COMPRESSED-CONTENT
是被壓縮後的數據,COMPRESSED-LEN
則是該數據的字節長度。在其他情況下,程序直接以普通字節序列的方式來保存字符串。比如說,對於一個長度爲
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
個節點值。節點值的保存方式和字符串的保存方式一樣。當進行載入時,讀入器讀取節點的數量,創建一個新的鏈表,然後一直執行以下步驟,直到指定節點數量滿足爲止:
- 讀取字符串表示的節點值
- 將包含節點值的新節點添加到鏈表中
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
個有序集元素的分值。當進行載入時,讀入器讀取有序集元素數量,創建一個新的有序集,然後一直執行以下步驟,直到指定元素數量滿足爲止:
- 讀入字符串形式保存的成員
member
- 讀入字符串形式保存的分值
score
,並將它轉換爲浮點數 - 添加
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
次:- 讀入一個字符串
- 再讀入另一個字符串
- 將第一個讀入的字符串作爲鍵,第二個讀入的字符串作爲值,插入到新建立的哈希中。
REDIS_LIST
類型、REDIS_HASH
類型和REDIS_ZSET
類型都使用了REDIS_ENCODING_ZIPLIST
編碼,ziplist
在 RDB 中的保存方式如下:+-----+---------+ | LEN | ZIPLIST | +-----+---------+
載入時,讀入器先讀入
ziplist
的字節長,再根據該字節長讀入數據,最後將數據還原成一個ziplist
。REDIS_ENCODING_INTSET
編碼的REDIS_SET
類型值保存爲以下結構:+-----+--------+ | LEN | INTSET | +-----+--------+
載入時,讀入器先讀入
intset
的字節長度,再根據長度讀入數據,最後將數據還原成intset
。
小結
rdbSave
會將數據庫數據保存到 RDB 文件,並在保存完成之前阻塞調用者。SAVE 命令直接調用
rdbSave
,阻塞 Redis 主進程; BGSAVE 用子進程調用rdbSave
,主進程仍可繼續處理命令請求。SAVE 執行期間, AOF 寫入可以在後臺線程進行, BGREWRITEAOF 可以在子進程進行,所以這三種操作可以同時進行。
爲了避免性能問題, 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 文件使用不同的格式來保存不同類型的值。