在本章之前的內容中, 我們介紹了 Redis 服務器保存和載入 RDB 文件的方法, 在這一節, 我們將對 RDB 文件本身進行介紹, 并詳細說明文件各個部分的結構和意義。
圖 IMAGE_RDB_STRUCT_OVERVIEW 展示了一個完整 RDB 文件所包含的各個部分。

注意
為了方便區分變量、數據、常量, 圖 IMAGE_RDB_STRUCT_OVERVIEW 中用全大寫單詞標示常量, 用全小寫單詞標示變量和數據。
本章展示的所有 RDB 文件結構圖都遵循這一規則。
RDB 文件的最開頭是?`REDIS`?部分, 這個部分的長度為?`5`?字節, 保存著?`"REDIS"`?五個字符。 通過這五個字符, 程序可以在載入文件時, 快速檢查所載入的文件是否 RDB 文件。
注意
因為 RDB 文件保存的是二進制數據, 而不是 C 字符串, 為了簡便起見, 我們用?`"REDIS"`?符號代表?`'R'`?、?`'E'`?、?`'D'`?、?`'I'`?、?`'S'`五個字符, 而不是帶?`'\0'`?結尾符號的 C 字符串?`'R'`?、?`'E'`?、?`'D'`?、?`'I'`?、?`'S'`?、?`'\0'`?。
本章介紹的所有內容,以及展示的所有 RDB 文件結構圖都遵循這一規則。
`db_version`?長度為?`4`?字節, 它的值是一個字符串表示的整數, 這個整數記錄了 RDB 文件的版本號, 比如?`"0006"`?就代表 RDB 文件的版本為第六版。 本章只介紹第六版 RDB 文件的結構。
`databases`?部分包含著零個或任意多個數據庫, 以及各個數據庫中的鍵值對數據:
* 如果服務器的數據庫狀態為空(所有數據庫都是空的), 那么這個部分也為空, 長度為?`0`?字節。
* 如果服務器的數據庫狀態為非空(有至少一個數據庫非空), 那么這個部分也為非空, 根據數據庫所保存鍵值對的數量、類型和內容不同, 這個部分的長度也會有所不同。
`EOF`?常量的長度為?`1`?字節, 這個常量標志著 RDB 文件正文內容的結束, 當讀入程序遇到這個值的時候, 它知道所有數據庫的所有鍵值對都已經載入完畢了。
`check_sum`?是一個?`8`?字節長的無符號整數, 保存著一個校驗和, 這個校驗和是程序通過對?`REDIS`?、?`db_version`?、?`databases`?、?`EOF`?四個部分的內容進行計算得出的。 服務器在載入 RDB 文件時, 會將載入數據所計算出的校驗和與?`check_sum`?所記錄的校驗和進行對比, 以此來檢查 RDB 文件是否有出錯或者損壞的情況出現。
作為例子, 圖 IMAGE_RDB_WITH_EMPTY_DATABASE 展示了一個?`databases`?部分為空的 RDB 文件: 文件開頭的?`"REDIS"`?表示這是一個 RDB 文件, 之后的?`"0006"`?表示這是第六版的 RDB 文件, 因為?`databases`?為空, 所以版本號之后直接跟著?`EOF`?常量, 最后的?`6265312314761917404`是文件的校驗和。

## databases 部分
一個 RDB 文件的?`databases`?部分可以保存任意多個非空數據庫。
比如說, 如果服務器的?`0`?號數據庫和?`3`?號數據庫非空, 那么服務器將創建一個如圖 IMAGE_RDB_WITH_TWO_DB 所示的 RDB 文件, 圖中的`database?0`?代表?`0`?號數據庫中的所有鍵值對數據, 而?`database?3`?則代表?`3`?號數據庫中的所有鍵值對數據。

每個非空數據庫在 RDB 文件中都可以保存為?`SELECTDB`?、?`db_number`?、?`key_value_pairs`?三個部分, 如圖 IMAGE_DATABASE_STRUCT_OF_RDB 所示。

`SELECTDB`?常量的長度為?`1`?字節, 當讀入程序遇到這個值的時候, 它知道接下來要讀入的將是一個數據庫號碼。
`db_number`?保存著一個數據庫號碼, 根據號碼的大小不同, 這個部分的長度可以是?`1`?字節、?`2`?字節或者?`5`?字節。 當程序讀入?`db_number`?部分之后, 服務器會調用?SELECT?命令, 根據讀入的數據庫號碼進行數據庫切換, 使得之后讀入的鍵值對可以載入到正確的數據庫中。
`key_value_pairs`?部分保存了數據庫中的所有鍵值對數據, 如果鍵值對帶有過期時間, 那么過期時間也會和鍵值對保存在一起。 根據鍵值對的數量、類型、內容、以及是否有過期時間等條件的不同,?`key_value_pairs`?部分的長度也會有所不同。
作為例子, 圖 IMAGE_EXAMPLE_OF_DB 展示了 RDB 文件中,?`0`?號數據庫的結構。

另外, 圖 IMAGE_RDB_WITH_DB_0_AND_DB_3 則展示了一個完整的 RDB 文件, 文件中包含了?`0`?號數據庫和?`3`?號數據庫。

## key_value_pairs 部分
RDB 文件中的每個?`key_value_pairs`?部分都保存了一個或以上數量的鍵值對, 如果鍵值對帶有過期時間的話, 那么鍵值對的過期時間也會被保存在內。
不帶過期時間的鍵值對在 RDB 文件中對由?`TYPE`?、?`key`?、?`value`?三部分組成, 如圖 IMAGE_KEY_WITHOUT_EXPIRE_TIME 所示。

`TYPE`?記錄了?`value`?的類型, 長度為?`1`?字節, 值可以是以下常量的其中一個:
* `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`?常量都代表了一種對象類型或者底層編碼, 當服務器讀入 RDB 文件中的鍵值對數據時, 程序會根據?`TYPE`?的值來決定如何讀入和解釋?`value`?的數據。
`key`?和?`value`?分別保存了鍵值對的鍵對象和值對象:
* 其中?`key`?總是一個字符串對象, 它的編碼方式和?`REDIS_RDB_TYPE_STRING`?類型的?`value`?一樣。 根據內容長度的不同,?`key`?的長度也會有所不同。
* 根據?`TYPE`?類型的不同, 以及保存內容長度的不同, 保存?`value`?的結構和長度也會有所不同, 本節稍后會詳細說明每種?`TYPE`?類型的`value`?結構保存方式。
帶有過期時間的鍵值對在 RDB 文件中的結構如圖?`IMAGE_KEY_WITH_EXPIRE_TIME`?所示。

帶有過期時間的鍵值對中的?`TYPE`?、?`key`?、?`value`?三個部分的意義, 和前面介紹的不帶過期時間的鍵值對的?`TYPE`?、?`key`?、?`value`?三個部分的意義完全相同, 至于新增的?`EXPIRETIME_MS`?和?`ms`?, 它們的意義如下:
* `EXPIRETIME_MS`?常量的長度為?`1`?字節, 它告知讀入程序, 接下來要讀入的將是一個以毫秒為單位的過期時間。
* `ms`?是一個?`8`?字節長的帶符號整數, 記錄著一個以毫秒為單位的 UNIX 時間戳, 這個時間戳就是鍵值對的過期時間。
作為例子, 圖 IMAGE_EXAMPLE_OF_KEY_WITHOUT_EXPIRE_TIME 展示了一個沒有過期時間的字符串鍵值對。

圖 IMAGE_EXAMPLE_OF_KEY_WITH_EXPIRE_TIME 展示了一個帶有過期時間的集合鍵值對, 其中鍵的過期時間為?`1388556000000`?(2014 年 1 月 1 日零時)。

## value 的編碼
RDB 文件中的每個?`value`?部分都保存了一個值對象, 每個值對象的類型都由與之對應的?`TYPE`?記錄, 根據類型的不同,?`value`?部分的結構、長度也會有所不同。
在接下來的各個小節中, 我們將分別介紹各種不同類型的值對象在 RDB 文件中的保存結構。
注意
本節接下來說到的各種?`REDIS_ENCODING_*`?編碼曾經在《對象》一章中介紹過, 如果忘記了可以去回顧一下。
### 字符串對象
如果?`TYPE`?的值為?`REDIS_RDB_TYPE_STRING`?, 那么?`value`?保存的就是一個字符串對象, 字符串對象的編碼可以是?`REDIS_ENCODING_INT`?或者`REDIS_ENCODING_RAW`?。
如果字符串對象的編碼為?`REDIS_ENCODING_INT`?, 那么說明對象中保存的是長度不超過?`32`?位的整數, 這種編碼的對象將以圖 IMAGE_INT_ENCODING_STRING 所示的結構保存。

其中,?`ENCODING`?的值可以是?`REDIS_RDB_ENC_INT8`?、?`REDIS_RDB_ENC_INT16`?或者?`REDIS_RDB_ENC_INT32`?三個常量的其中一個, 它們分別代表 RDB 文件使用?`8`?位(bit)、?`16`?位或者?`32`?位來保存整數值?`integer`?。
舉個例子, 如果字符串對象中保存的是可以用?`8`?位來保存的整數?`123`?, 那么這個對象在 RDB 文件中保存的結構將如圖 IMAGE_EXAMPLE_OF_INT_ENCODING_STRING 所示。

如果字符串對象的編碼為?`REDIS_ENCODING_RAW`?, 那么說明對象所保存的是一個字符串值, 根據字符串長度的不同, 有壓縮和不壓縮兩種方法來保存這個字符串:
* 如果字符串的長度小于等于?`20`?字節, 那么這個字符串會直接被原樣保存。
* 如果字符串的長度大于?`20`?字節, 那么這個字符串會被壓縮之后再保存。
注意
以上兩個條件是在假設服務器打開了 RDB 文件壓縮功能的情況下進行的, 如果服務器關閉了 RDB 文件壓縮功能, 那么 RDB 程序總以無壓縮的方式保存字符串值。
具體信息可以參考?`redis.conf`?文件中關于?`rdbcompression`?選項的說明。
對于沒有被壓縮的字符串, RDB 程序會以圖 IMAGE_NON_COMPRESS_STRING 所示的結構來保存該字符串。

其中,?`string`?部分保存了字符串值本身,而?`len`?保存了字符串值的長度。
對于壓縮后的字符串, RDB 程序會以圖 IMAGE_COMPRESSED_STRING 所示的結構來保存該字符串。

其中,?`REDIS_RDB_ENC_LZF`?常量標志著字符串已經被 LZF 算法([http://liblzf.plan9.de](http://liblzf.plan9.de/))壓縮過了, 讀入程序在碰到這個常量時, 會根據之后的?`compressed_len`?、?`origin_len`?和?`compressed_string`?三部分, 對字符串進行解壓縮: 其中?`compressed_len`?記錄的是字符串被壓縮之后的長度, 而?`origin_len`?記錄的是字符串原來的長度,?`compressed_string`?記錄的則是被壓縮之后的字符串。
圖 IMAGE_EXAMPLE_OF_NON_COMPRESS_STRING 展示了一個保存無壓縮字符串的例子, 其中字符串的長度為?`5`?, 字符串的值為?`"hello"`?。

圖 IMAGE_EXAMPLE_OF_COMPRESS_STRING 展示了一個壓縮后的字符串示例, 從圖中可以看出, 字符串原本的長度為?`21`?, 壓縮之后的長度為`6`?, 壓縮之后的字符串內容為?`"?aa???"`?, 其中?`?`?代表的是無法用字符串形式打印出來的字節。

### 列表對象
如果?`TYPE`?的值為?`REDIS_RDB_TYPE_LIST`?, 那么?`value`?保存的就是一個?`REDIS_ENCODING_LINKEDLIST`?編碼的列表對象, RDB 文件保存這種對象的結構如圖 IMAGE_LINKEDLIST_ENCODING_LIST 所示。

`list_length`?記錄了列表的長度, 它記錄列表保存了多少個項(item), 讀入程序可以通過這個長度知道自己應該讀入多少個列表項。
圖中以?`item`?開頭的部分代表列表的項, 因為每個列表項都是一個字符串對象, 所以程序會以處理字符串對象的方式來保存和讀入列表項。
作為示例, 圖 IMAGE_EXAMPLE_OF_LINKEDLIST_ENCODING_LIST 展示了一個包含三個元素的列表。

結構中的第一個數字?`3`?是列表的長度, 之后跟著的分別是第一個列表項、第二個列表項和第三個列表項, 其中:
* 第一個列表項的長度為?`5`?, 內容為字符串?`"hello"`?。
* 第二個列表項的長度也為?`5`?, 內容為字符串?`"world"`?。
* 第三個列表項的長度為?`1`?, 內容為字符串?`"!"`?。
### 集合對象
如果?`TYPE`?的值為?`REDIS_RDB_TYPE_SET`?, 那么?`value`?保存的就是一個?`REDIS_ENCODING_HT`?編碼的集合對象, RDB 文件保存這種對象的結構如圖 IMAGE_HT_ENCODING_SET 所示。

其中,?`set_size`?是集合的大小, 它記錄集合保存了多少個元素, 讀入程序可以通過這個大小知道自己應該讀入多少個集合元素。
圖中以?`elem`?開頭的部分代表集合的元素, 因為每個集合元素都是一個字符串對象, 所以程序會以處理字符串對象的方式來保存和讀入集合元素。
作為示例, 圖 IMAGE_EXAMPLE_OF_HT_SET 展示了一個包含四個元素的集合。

結構中的第一個數字?`4`?記錄了集合的大小, 之后跟著的是集合的四個元素:
* 第一個元素的長度為?`5`?,值為?`"apple"`?。
* 第二個元素的長度為?`6`?,值為?`"banana"`?。
* 第三個元素的長度為?`3`?,值為?`"cat"`?。
* 第四個元素的長度為?`3`?,值為?`"dog"`?。
### 哈希表對象
如果?`TYPE`?的值為?`REDIS_RDB_TYPE_HASH`?, 那么?`value`?保存的就是一個?`REDIS_ENCODING_HT`?編碼的集合對象, RDB 文件保存這種對象的結構如圖 IMAGE_HT_HASH 所示:
* `hash_size`?記錄了哈希表的大小, 也即是這個哈希表保存了多少鍵值對, 讀入程序可以通過這個大小知道自己應該讀入多少個鍵值對。
* 以?`key_value_pair`?開頭的部分代表哈希表中的鍵值對, 鍵值對的鍵和值都是字符串對象, 所以程序會以處理字符串對象的方式來保存和讀入鍵值對。

結構中的每個鍵值對都以鍵緊挨著值的方式排列在一起, 如圖 IMAGE_KEY_VALUE_PAIR_OF_HT_HASH 所示。

因此, 從更詳細的角度看, 圖 IMAGE_HT_HASH 所展示的結構可以進一步修改為圖 IMAGE_DETIAL_HT_HASH 。

作為示例, 圖 IMAGE_EXAMPLE_OF_HT_HASH 展示了一個包含兩個鍵值對的哈希表。

在這個示例結構中, 第一個數字?`2`?記錄了哈希表的鍵值對數量, 之后跟著的是兩個鍵值對:
* 第一個鍵值對的鍵是長度為?`1`?的字符串?`"a"`?, 值是長度為?`5`?的字符串?`"apple"`?。
* 第二個鍵值對的鍵是長度為?`1`?的字符串?`"b"`?, 值是長度為?`6`?的字符串?`"banana"`?。
### 有序集合對象
如果?`TYPE`?的值為?`REDIS_RDB_TYPE_ZSET`?, 那么?`value`?保存的就是一個?`REDIS_ENCODING_SKIPLIST`?編碼的有序集合對象, RDB 文件保存這種對象的結構如圖 IMAGE_SKIPLIST_ZSET 所示。

`sorted_set_size`?記錄了有序集合的大小, 也即是這個有序集合保存了多少元素, 讀入程序需要根據這個值來決定應該讀入多少有序集合元素。
以?`element`?開頭的部分代表有序集合中的元素, 每個元素又分為成員(member)和分值(score)兩部分, 成員是一個字符串對象, 分值則是一個?`double`?類型的浮點數, 程序在保存 RDB 文件時會先將分值轉換成字符串對象, 然后再用保存字符串對象的方法將分值保存起來。
有序集合中的每個元素都以成員緊挨著分值的方式排列, 如圖 IMAGE_MEMBER_AND_SCORE_OF_ZSET 所示。

因此, 從更詳細的角度看, 圖 IMAGE_SKIPLIST_ZSET 所展示的結構可以進一步修改為圖 IMAGE_DETIAL_SKIPLIST_ZSET 。

作為示例, 圖 IMAGE_EXAMPLE_OF_SKIPLIST_ZSET 展示了一個帶有兩個元素的有序集合。

在這個示例結構中, 第一個數字?`2`?記錄了有序集合的元素數量, 之后跟著的是兩個有序集合元素:
* 第一個元素的成員是長度為?`2`?的字符串?`"pi"`?, 分值被轉換成字符串之后變成了長度為?`4`?的字符串?`"3.14"`?。
* 第二個元素的成員是長度為?`1`?的字符串?`"e"`?, 分值被轉換成字符串之后變成了長度為?`3`?的字符串?`"2.7"`?。
### INTSET 編碼的集合
如果?`TYPE`?的值為?`REDIS_RDB_TYPE_SET_INTSET`?, 那么?`value`?保存的就是一個整數集合對象, RDB 文件保存這種對象的方法是, 先將整數集合轉換為字符串對象, 然后將這個字符串對象保存到 RDB 文件里面。
如果程序在讀入 RDB 文件的過程中, 碰到由整數集合對象轉換成的字符串對象, 那么程序會根據?`TYPE`?值的指示, 先讀入字符串對象, 再將這個字符串對象轉換成原來的整數集合對象。
### ZIPLIST 編碼的列表、哈希表或者有序集合
如果?`TYPE`?的值為?`REDIS_RDB_TYPE_LIST_ZIPLIST`?、?`REDIS_RDB_TYPE_HASH_ZIPLIST`?或者?`REDIS_RDB_TYPE_ZSET_ZIPLIST`?, 那么?`value`?保存的就是一個壓縮列表對象, RDB 文件保存這種對象的方法是:
1. 將壓縮列表轉換成一個字符串對象。
2. 將轉換所得的字符串對象保存到 RDB 文件。
如果程序在讀入 RDB 文件的過程中, 碰到由壓縮列表對象轉換成的字符串對象, 那么程序會根據?`TYPE`?值的指示, 執行以下操作:
1. 讀入字符串對象,并將它轉換成原來的壓縮列表對象。
2. 根據?`TYPE`?的值,設置壓縮列表對象的類型: 如果?`TYPE`?的值為?`REDIS_RDB_TYPE_LIST_ZIPLIST`?, 那么壓縮列表對象的類型為列表; 如果`TYPE`?的值為?`REDIS_RDB_TYPE_HASH_ZIPLIST`?, 那么壓縮列表對象的類型為哈希表; 如果?`TYPE`?的值為?`REDIS_RDB_TYPE_ZSET_ZIPLIST`?, 那么壓縮列表對象的類型為有序集合。
從步驟 2 可以看出, 由于?`TYPE`?的存在, 即使列表、哈希表和有序集合三種類型都使用壓縮列表來保存, RDB 讀入程序也總可以將讀入并轉換之后得出的壓縮列表設置成原來的類型。
- 介紹
- 前言
- 致謝
- 簡介
- 第一部分:數據結構與對象
- 簡單動態字符串
- SDS 的定義
- SDS 與 C 字符串的區別
- SDS API
- 重點回顧
- 參考資料
- 鏈表
- 鏈表和鏈表節點的實現
- 鏈表和鏈表節點的 API
- 重點回顧
- 字典
- 字典的實現
- 哈希算法
- 解決鍵沖突
- rehash
- 漸進式 rehash
- 字典 API
- 重點回顧
- 跳躍表
- 跳躍表的實現
- 跳躍表 API
- 重點回顧
- 整數集合
- 整數集合的實現
- 升級
- 升級的好處
- 降級
- 整數集合 API
- 重點回顧
- 壓縮列表
- 壓縮列表的構成
- 壓縮列表節點的構成
- 連鎖更新
- 壓縮列表 API
- 重點回顧
- 對象
- 對象的類型與編碼
- 字符串對象
- 列表對象
- 哈希對象
- 集合對象
- 有序集合對象
- 類型檢查與命令多態
- 內存回收
- 對象共享
- 對象的空轉時長
- 重點回顧
- 第二部分:單機數據庫的實現
- 數據庫
- 數據庫鍵空間
- 重點回顧
- RDB 持久化
- RDB 文件結構
- 重點回顧
- AOF 持久化
- AOF 持久化的實現
- 重點回顧
- 事件
- 文件事件
- 重點回顧
- 參考資料
- 客戶端
- 客戶端屬性
- 重點回顧
- 服務器
- 命令請求的執行過程
- 重點回顧
- 第三部分:多機數據庫的實現
- 復制
- 舊版復制功能的實現
- 重點回顧
- Sentinel
- 啟動并初始化 Sentinel
- 重點回顧
- 參考資料
- 集群
- 節點
- 重點回顧
- 第四部分:獨立功能的實現
- 發布與訂閱
- 頻道的訂閱與退訂
- 重點回顧
- 參考資料
- 事務
- 事務的實現
- 重點回顧
- Lua 腳本
- 創建并修改 Lua 環境
- 重點回顧
- 排序
- SORT <key> 命令的實現
- 重點回顧
- 二進制位數組
- GETBIT 命令的實現
- 重點回顧
- 慢查詢日志
- 慢查詢記錄的保存
- 慢查詢日志的閱覽和刪除
- 添加新日志
- 重點回顧
- 監視器
- 成為監視器
- 向監視器發送命令信息
- 重點回顧
- 源碼、相關資源和勘誤