在本章之前的內(nèi)容中, 我們介紹了 Redis 服務(wù)器保存和載入 RDB 文件的方法, 在這一節(jié), 我們將對 RDB 文件本身進(jìn)行介紹, 并詳細(xì)說明文件各個部分的結(jié)構(gòu)和意義。
圖 IMAGE_RDB_STRUCT_OVERVIEW 展示了一個完整 RDB 文件所包含的各個部分。
注意
為了方便區(qū)分變量、數(shù)據(jù)、常量, 圖 IMAGE_RDB_STRUCT_OVERVIEW 中用全大寫單詞標(biāo)示常量, 用全小寫單詞標(biāo)示變量和數(shù)據(jù)。
本章展示的所有 RDB 文件結(jié)構(gòu)圖都遵循這一規(guī)則。
RDB 文件的最開頭是 REDIS
部分, 這個部分的長度為 5
字節(jié), 保存著 "REDIS"
五個字符。 通過這五個字符, 程序可以在載入文件時, 快速檢查所載入的文件是否 RDB 文件。
注意
因為 RDB 文件保存的是二進(jìn)制數(shù)據(jù), 而不是 C 字符串, 為了簡便起見, 我們用 "REDIS"
符號代表 'R'
、 'E'
、 'D'
、 'I'
、 'S'
五個字符, 而不是帶 '\0'
結(jié)尾符號的 C 字符串 'R'
、 'E'
、 'D'
、 'I'
、 'S'
、 '\0'
。
本章介紹的所有內(nèi)容,以及展示的所有 RDB 文件結(jié)構(gòu)圖都遵循這一規(guī)則。
db_version
長度為 4
字節(jié), 它的值是一個字符串表示的整數(shù), 這個整數(shù)記錄了 RDB 文件的版本號, 比如 "0006"
就代表 RDB 文件的版本為第六版。 本章只介紹第六版 RDB 文件的結(jié)構(gòu)。
databases
部分包含著零個或任意多個數(shù)據(jù)庫, 以及各個數(shù)據(jù)庫中的鍵值對數(shù)據(jù):
0
字節(jié)。EOF
常量的長度為 1
字節(jié), 這個常量標(biāo)志著 RDB 文件正文內(nèi)容的結(jié)束, 當(dāng)讀入程序遇到這個值的時候, 它知道所有數(shù)據(jù)庫的所有鍵值對都已經(jīng)載入完畢了。
check_sum
是一個 8
字節(jié)長的無符號整數(shù), 保存著一個校驗和, 這個校驗和是程序通過對 REDIS
、 db_version
、 databases
、 EOF
四個部分的內(nèi)容進(jìn)行計算得出的。 服務(wù)器在載入 RDB 文件時, 會將載入數(shù)據(jù)所計算出的校驗和與 check_sum
所記錄的校驗和進(jìn)行對比, 以此來檢查 RDB 文件是否有出錯或者損壞的情況出現(xiàn)。
作為例子, 圖 IMAGE_RDB_WITH_EMPTY_DATABASE 展示了一個 databases
部分為空的 RDB 文件: 文件開頭的 "REDIS"
表示這是一個 RDB 文件, 之后的 "0006"
表示這是第六版的 RDB 文件, 因為 databases
為空, 所以版本號之后直接跟著 EOF
常量, 最后的 6265312314761917404
是文件的校驗和。
一個 RDB 文件的 databases
部分可以保存任意多個非空數(shù)據(jù)庫。
比如說, 如果服務(wù)器的 0
號數(shù)據(jù)庫和 3
號數(shù)據(jù)庫非空, 那么服務(wù)器將創(chuàng)建一個如圖 IMAGE_RDB_WITH_TWO_DB 所示的 RDB 文件, 圖中的database 0
代表 0
號數(shù)據(jù)庫中的所有鍵值對數(shù)據(jù), 而 database 3
則代表 3
號數(shù)據(jù)庫中的所有鍵值對數(shù)據(jù)。
每個非空數(shù)據(jù)庫在 RDB 文件中都可以保存為 SELECTDB
、 db_number
、 key_value_pairs
三個部分, 如圖 IMAGE_DATABASE_STRUCT_OF_RDB 所示。
SELECTDB
常量的長度為 1
字節(jié), 當(dāng)讀入程序遇到這個值的時候, 它知道接下來要讀入的將是一個數(shù)據(jù)庫號碼。
db_number
保存著一個數(shù)據(jù)庫號碼, 根據(jù)號碼的大小不同, 這個部分的長度可以是 1
字節(jié)、 2
字節(jié)或者 5
字節(jié)。 當(dāng)程序讀入 db_number
部分之后, 服務(wù)器會調(diào)用 SELECT 命令, 根據(jù)讀入的數(shù)據(jù)庫號碼進(jìn)行數(shù)據(jù)庫切換, 使得之后讀入的鍵值對可以載入到正確的數(shù)據(jù)庫中。
key_value_pairs
部分保存了數(shù)據(jù)庫中的所有鍵值對數(shù)據(jù), 如果鍵值對帶有過期時間, 那么過期時間也會和鍵值對保存在一起。 根據(jù)鍵值對的數(shù)量、類型、內(nèi)容、以及是否有過期時間等條件的不同, key_value_pairs
部分的長度也會有所不同。
作為例子, 圖 IMAGE_EXAMPLE_OF_DB 展示了 RDB 文件中, 0
號數(shù)據(jù)庫的結(jié)構(gòu)。
另外, 圖 IMAGE_RDB_WITH_DB_0_AND_DB_3 則展示了一個完整的 RDB 文件, 文件中包含了 0
號數(shù)據(jù)庫和 3
號數(shù)據(jù)庫。
RDB 文件中的每個 key_value_pairs
部分都保存了一個或以上數(shù)量的鍵值對, 如果鍵值對帶有過期時間的話, 那么鍵值對的過期時間也會被保存在內(nèi)。
不帶過期時間的鍵值對在 RDB 文件中對由 TYPE
、 key
、 value
三部分組成, 如圖 IMAGE_KEY_WITHOUT_EXPIRE_TIME 所示。
TYPE
記錄了 value
的類型, 長度為 1
字節(jié), 值可以是以下常量的其中一個:
REDIS_RDB_TYPE_STRING
REDIS_RDB_TYPE_LIST
REDIS_RDB_TYPE_SET
REDIS_RDB_TYPE_ZSET
REDIS_RDB_TYPE_HASH
REDIS_RDB_TYPE_LIST_ZIPLIST
REDIS_RDB_TYPE_SET_INTSET
REDIS_RDB_TYPE_ZSET_ZIPLIST
REDIS_RDB_TYPE_HASH_ZIPLIST
以上列出的每個 TYPE
常量都代表了一種對象類型或者底層編碼, 當(dāng)服務(wù)器讀入 RDB 文件中的鍵值對數(shù)據(jù)時, 程序會根據(jù) TYPE
的值來決定如何讀入和解釋 value
的數(shù)據(jù)。
key
和 value
分別保存了鍵值對的鍵對象和值對象:
key
總是一個字符串對象, 它的編碼方式和 REDIS_RDB_TYPE_STRING
類型的 value
一樣。 根據(jù)內(nèi)容長度的不同, key
的長度也會有所不同。TYPE
類型的不同, 以及保存內(nèi)容長度的不同, 保存 value
的結(jié)構(gòu)和長度也會有所不同, 本節(jié)稍后會詳細(xì)說明每種 TYPE
類型的value
結(jié)構(gòu)保存方式。帶有過期時間的鍵值對在 RDB 文件中的結(jié)構(gòu)如圖 IMAGE_KEY_WITH_EXPIRE_TIME
所示。
帶有過期時間的鍵值對中的 TYPE
、 key
、 value
三個部分的意義, 和前面介紹的不帶過期時間的鍵值對的 TYPE
、 key
、 value
三個部分的意義完全相同, 至于新增的 EXPIRETIME_MS
和 ms
, 它們的意義如下:
EXPIRETIME_MS
常量的長度為 1
字節(jié), 它告知讀入程序, 接下來要讀入的將是一個以毫秒為單位的過期時間。ms
是一個 8
字節(jié)長的帶符號整數(shù), 記錄著一個以毫秒為單位的 UNIX 時間戳, 這個時間戳就是鍵值對的過期時間。作為例子, 圖 IMAGE_EXAMPLE_OF_KEY_WITHOUT_EXPIRE_TIME 展示了一個沒有過期時間的字符串鍵值對。
圖 IMAGE_EXAMPLE_OF_KEY_WITH_EXPIRE_TIME 展示了一個帶有過期時間的集合鍵值對, 其中鍵的過期時間為 1388556000000
(2014 年 1 月 1 日零時)。
RDB 文件中的每個 value
部分都保存了一個值對象, 每個值對象的類型都由與之對應(yīng)的 TYPE
記錄, 根據(jù)類型的不同, value
部分的結(jié)構(gòu)、長度也會有所不同。
在接下來的各個小節(jié)中, 我們將分別介紹各種不同類型的值對象在 RDB 文件中的保存結(jié)構(gòu)。
注意
本節(jié)接下來說到的各種 REDIS_ENCODING_*
編碼曾經(jīng)在《對象》一章中介紹過, 如果忘記了可以去回顧一下。
如果 TYPE
的值為 REDIS_RDB_TYPE_STRING
, 那么 value
保存的就是一個字符串對象, 字符串對象的編碼可以是 REDIS_ENCODING_INT
或者REDIS_ENCODING_RAW
。
如果字符串對象的編碼為 REDIS_ENCODING_INT
, 那么說明對象中保存的是長度不超過 32
位的整數(shù), 這種編碼的對象將以圖 IMAGE_INT_ENCODING_STRING 所示的結(jié)構(gòu)保存。
其中, ENCODING
的值可以是 REDIS_RDB_ENC_INT8
、 REDIS_RDB_ENC_INT16
或者 REDIS_RDB_ENC_INT32
三個常量的其中一個, 它們分別代表 RDB 文件使用 8
位(bit)、 16
位或者 32
位來保存整數(shù)值 integer
。
舉個例子, 如果字符串對象中保存的是可以用 8
位來保存的整數(shù) 123
, 那么這個對象在 RDB 文件中保存的結(jié)構(gòu)將如圖 IMAGE_EXAMPLE_OF_INT_ENCODING_STRING 所示。
如果字符串對象的編碼為 REDIS_ENCODING_RAW
, 那么說明對象所保存的是一個字符串值, 根據(jù)字符串長度的不同, 有壓縮和不壓縮兩種方法來保存這個字符串:
20
字節(jié), 那么這個字符串會直接被原樣保存。20
字節(jié), 那么這個字符串會被壓縮之后再保存。注意
以上兩個條件是在假設(shè)服務(wù)器打開了 RDB 文件壓縮功能的情況下進(jìn)行的, 如果服務(wù)器關(guān)閉了 RDB 文件壓縮功能, 那么 RDB 程序總以無壓縮的方式保存字符串值。
具體信息可以參考 redis.conf
文件中關(guān)于 rdbcompression
選項的說明。
對于沒有被壓縮的字符串, RDB 程序會以圖 IMAGE_NON_COMPRESS_STRING 所示的結(jié)構(gòu)來保存該字符串。
其中, string
部分保存了字符串值本身,而 len
保存了字符串值的長度。
對于壓縮后的字符串, RDB 程序會以圖 IMAGE_COMPRESSED_STRING 所示的結(jié)構(gòu)來保存該字符串。
其中, REDIS_RDB_ENC_LZF
常量標(biāo)志著字符串已經(jīng)被 LZF 算法(http://liblzf.plan9.de)壓縮過了, 讀入程序在碰到這個常量時, 會根據(jù)之后的 compressed_len
、 origin_len
和 compressed_string
三部分, 對字符串進(jìn)行解壓縮: 其中 compressed_len
記錄的是字符串被壓縮之后的長度, 而 origin_len
記錄的是字符串原來的長度, compressed_string
記錄的則是被壓縮之后的字符串。
圖 IMAGE_EXAMPLE_OF_NON_COMPRESS_STRING 展示了一個保存無壓縮字符串的例子, 其中字符串的長度為 5
, 字符串的值為 "hello"
。
圖 IMAGE_EXAMPLE_OF_COMPRESS_STRING 展示了一個壓縮后的字符串示例, 從圖中可以看出, 字符串原本的長度為 21
, 壓縮之后的長度為6
, 壓縮之后的字符串內(nèi)容為 "?aa???"
, 其中 ?
代表的是無法用字符串形式打印出來的字節(jié)。
如果 TYPE
的值為 REDIS_RDB_TYPE_LIST
, 那么 value
保存的就是一個 REDIS_ENCODING_LINKEDLIST
編碼的列表對象, RDB 文件保存這種對象的結(jié)構(gòu)如圖 IMAGE_LINKEDLIST_ENCODING_LIST 所示。
list_length
記錄了列表的長度, 它記錄列表保存了多少個項(item), 讀入程序可以通過這個長度知道自己應(yīng)該讀入多少個列表項。
圖中以 item
開頭的部分代表列表的項, 因為每個列表項都是一個字符串對象, 所以程序會以處理字符串對象的方式來保存和讀入列表項。
作為示例, 圖 IMAGE_EXAMPLE_OF_LINKEDLIST_ENCODING_LIST 展示了一個包含三個元素的列表。
結(jié)構(gòu)中的第一個數(shù)字 3
是列表的長度, 之后跟著的分別是第一個列表項、第二個列表項和第三個列表項, 其中:
5
, 內(nèi)容為字符串 "hello"
。5
, 內(nèi)容為字符串 "world"
。1
, 內(nèi)容為字符串 "!"
。如果 TYPE
的值為 REDIS_RDB_TYPE_SET
, 那么 value
保存的就是一個 REDIS_ENCODING_HT
編碼的集合對象, RDB 文件保存這種對象的結(jié)構(gòu)如圖 IMAGE_HT_ENCODING_SET 所示。
其中, set_size
是集合的大小, 它記錄集合保存了多少個元素, 讀入程序可以通過這個大小知道自己應(yīng)該讀入多少個集合元素。
圖中以 elem
開頭的部分代表集合的元素, 因為每個集合元素都是一個字符串對象, 所以程序會以處理字符串對象的方式來保存和讀入集合元素。
作為示例, 圖 IMAGE_EXAMPLE_OF_HT_SET 展示了一個包含四個元素的集合。
結(jié)構(gòu)中的第一個數(shù)字 4
記錄了集合的大小, 之后跟著的是集合的四個元素:
5
,值為 "apple"
。6
,值為 "banana"
。3
,值為 "cat"
。3
,值為 "dog"
。如果 TYPE
的值為 REDIS_RDB_TYPE_HASH
, 那么 value
保存的就是一個 REDIS_ENCODING_HT
編碼的集合對象, RDB 文件保存這種對象的結(jié)構(gòu)如圖 IMAGE_HT_HASH 所示:
hash_size
記錄了哈希表的大小, 也即是這個哈希表保存了多少鍵值對, 讀入程序可以通過這個大小知道自己應(yīng)該讀入多少個鍵值對。key_value_pair
開頭的部分代表哈希表中的鍵值對, 鍵值對的鍵和值都是字符串對象, 所以程序會以處理字符串對象的方式來保存和讀入鍵值對。結(jié)構(gòu)中的每個鍵值對都以鍵緊挨著值的方式排列在一起, 如圖 IMAGE_KEY_VALUE_PAIR_OF_HT_HASH 所示。
因此, 從更詳細(xì)的角度看, 圖 IMAGE_HT_HASH 所展示的結(jié)構(gòu)可以進(jìn)一步修改為圖 IMAGE_DETIAL_HT_HASH 。
作為示例, 圖 IMAGE_EXAMPLE_OF_HT_HASH 展示了一個包含兩個鍵值對的哈希表。
在這個示例結(jié)構(gòu)中, 第一個數(shù)字 2
記錄了哈希表的鍵值對數(shù)量, 之后跟著的是兩個鍵值對:
1
的字符串 "a"
, 值是長度為 5
的字符串 "apple"
。1
的字符串 "b"
, 值是長度為 6
的字符串 "banana"
。如果 TYPE
的值為 REDIS_RDB_TYPE_ZSET
, 那么 value
保存的就是一個 REDIS_ENCODING_SKIPLIST
編碼的有序集合對象, RDB 文件保存這種對象的結(jié)構(gòu)如圖 IMAGE_SKIPLIST_ZSET 所示。
sorted_set_size
記錄了有序集合的大小, 也即是這個有序集合保存了多少元素, 讀入程序需要根據(jù)這個值來決定應(yīng)該讀入多少有序集合元素。
以 element
開頭的部分代表有序集合中的元素, 每個元素又分為成員(member)和分值(score)兩部分, 成員是一個字符串對象, 分值則是一個 double
類型的浮點數(shù), 程序在保存 RDB 文件時會先將分值轉(zhuǎn)換成字符串對象, 然后再用保存字符串對象的方法將分值保存起來。
有序集合中的每個元素都以成員緊挨著分值的方式排列, 如圖 IMAGE_MEMBER_AND_SCORE_OF_ZSET 所示。
因此, 從更詳細(xì)的角度看, 圖 IMAGE_SKIPLIST_ZSET 所展示的結(jié)構(gòu)可以進(jìn)一步修改為圖 IMAGE_DETIAL_SKIPLIST_ZSET 。
作為示例, 圖 IMAGE_EXAMPLE_OF_SKIPLIST_ZSET 展示了一個帶有兩個元素的有序集合。
在這個示例結(jié)構(gòu)中, 第一個數(shù)字 2
記錄了有序集合的元素數(shù)量, 之后跟著的是兩個有序集合元素:
2
的字符串 "pi"
, 分值被轉(zhuǎn)換成字符串之后變成了長度為 4
的字符串 "3.14"
。1
的字符串 "e"
, 分值被轉(zhuǎn)換成字符串之后變成了長度為 3
的字符串 "2.7"
。如果 TYPE
的值為 REDIS_RDB_TYPE_SET_INTSET
, 那么 value
保存的就是一個整數(shù)集合對象, RDB 文件保存這種對象的方法是, 先將整數(shù)集合轉(zhuǎn)換為字符串對象, 然后將這個字符串對象保存到 RDB 文件里面。
如果程序在讀入 RDB 文件的過程中, 碰到由整數(shù)集合對象轉(zhuǎn)換成的字符串對象, 那么程序會根據(jù) TYPE
值的指示, 先讀入字符串對象, 再將這個字符串對象轉(zhuǎn)換成原來的整數(shù)集合對象。
如果 TYPE
的值為 REDIS_RDB_TYPE_LIST_ZIPLIST
、 REDIS_RDB_TYPE_HASH_ZIPLIST
或者 REDIS_RDB_TYPE_ZSET_ZIPLIST
, 那么 value
保存的就是一個壓縮列表對象, RDB 文件保存這種對象的方法是:
如果程序在讀入 RDB 文件的過程中, 碰到由壓縮列表對象轉(zhuǎn)換成的字符串對象, 那么程序會根據(jù) TYPE
值的指示, 執(zhí)行以下操作:
TYPE
的值,設(shè)置壓縮列表對象的類型: 如果 TYPE
的值為 REDIS_RDB_TYPE_LIST_ZIPLIST
, 那么壓縮列表對象的類型為列表; 如果TYPE
的值為 REDIS_RDB_TYPE_HASH_ZIPLIST
, 那么壓縮列表對象的類型為哈希表; 如果 TYPE
的值為 REDIS_RDB_TYPE_ZSET_ZIPLIST
, 那么壓縮列表對象的類型為有序集合。從步驟 2 可以看出, 由于 TYPE
的存在, 即使列表、哈希表和有序集合三種類型都使用壓縮列表來保存, RDB 讀入程序也總可以將讀入并轉(zhuǎn)換之后得出的壓縮列表設(shè)置成原來的類型。
更多建議: