跟著大彬讀原始碼 - Redis 5 - 物件和資料型別(上)

北國丶風光發表於2019-07-22

相信很多人應該都知道 Redis 有五種資料型別:字串、列表、雜湊、集合和有序集合。但這五種資料型別是什麼含義?Redis 的資料又是怎樣儲存的?今天我們一起來認識下 Redis 這五種資料結構的含義及其底層實現。

首先要明確的是,Redis 並沒有直接使用這五種資料結構來實現鍵值對資料庫,而是基於這些資料結構建立了一套物件系統,我們常說的資料型別,準確來說,是 Redis 物件系統的型別。

1 物件

對於 Redis 而言,所有鍵值對的儲存,都是將資料儲存在物件結構中。所不同的是,鍵總是一個字串物件,值可以是任意型別的物件
物件原始碼結構如下:

typedef struct redisObject {
    unsigned type:4;       // 物件型別
    unsigned encoding:4;   // 物件編碼
    unsigned lru:LRU_BITS; // LRU
    int refcount;          // 引用統計
    void *ptr;             // 指向底層實現資料結構的指標
} robj;
  • type 欄位:物件型別,就是我們常說的。string、list、hash、set、zset。
  • encoding:物件編碼。也就是我們上面說的底層資料結構。
  • LRU:鍵值對的 LRU。
  • refcount:鍵值對物件的引用統計。當此值為 0 時,回收物件。
  • *ptr:指向底層實現資料結構的指標。就是實際存放資料的地址。

1.2 物件型別

物件有五種資料型別,就是我們上面提過的:

  1. 字串型別
  2. 列表型別
  3. 雜湊型別
  4. 集合型別
  5. 有序集合型別

結合我們上面提到的鍵值對儲存型別的差別,可以瞭解到,我們常說的“一個列表鍵或一個雜湊鍵”,本質上指的是:一個 key 對應的 value 是列表物件或雜湊物件

對於 type 欄位,我們可以使用 TYPE 命令來檢視指定 key 對應 value 值的物件型別。
圖 2 - 設定不同型別的 key
圖 3 - TYPE 命令對不同型別的 key 的輸出

1.3 物件編碼

按道理講,已經有了 type,為什麼還要搞個編碼呢?

想想看,通過 encoding 屬性,我們是不是使用不同編碼的物件?這種使用方式可以根據不同的使用場景來為一個物件設定不同的編碼,從而優化在某一場景下的效率,極大的提升了 Redis 的靈活性和效率。

舉個栗子,在列表物件包含的元素比較少時,Redis 使用壓縮列表作為列表物件的底層實現:

  • 壓縮列表比快速連結串列更節約記憶體,並且在元素數量較少時,在記憶體中以連續塊方式報錯的壓縮列表比起快速列表可以更快的載入到快取中;
  • 隨著列表物件包含的元素越來越多,使用壓縮列表儲存元素的優勢消失時,物件就會將底層實現從壓縮列表轉為功能更強、也更適合儲存大量元素的快速連結串列。

後面介紹完編碼型別後,我們會詳細認識不同型別對應的各個編碼方式。

encoding 屬性有以下取值:

  1. OBJ_ENCODING_RAW
  2. OBJ_ENCODING_INT
  3. OBJ_ENCODING_HT
  4. OBJ_ENCODING_QUICKLIST
  5. OBJ_ENCODING_ZIPLIST
  6. OBJ_ENCODING_INTSET
  7. OBJ_ENCODING_SKIPLIST
  8. OBJ_ENCODING_EMBSTR

物件的編碼型別可以由 OBJECT ENCODING 命令獲取。

圖 4 - 獲取 key 的編碼

OBJECT ENCODING 命令對應原始碼如下:

# src/object.c
char *strEncoding(int encoding) {
    switch(encoding) {
    case OBJ_ENCODING_RAW: return "raw";
    case OBJ_ENCODING_INT: return "int";
    case OBJ_ENCODING_HT: return "hashtable";
    case OBJ_ENCODING_QUICKLIST: return "quicklist";
    case OBJ_ENCODING_ZIPLIST: return "ziplist";
    case OBJ_ENCODING_INTSET: return "intset";
    case OBJ_ENCODING_SKIPLIST: return "skiplist";
    case OBJ_ENCODING_EMBSTR: return "embstr";
    default: return "unknown";
    }
}

OBJECT ENCODING 命令輸出值與 encoding 屬性取值對應關係如下:
| 物件使用的底層資料結構 | 編碼常量 | OBJECT ENCODING 輸出 |
| :-: | :-: | :-: |
| 簡單動態字串 |OBJ_ENCODING_RAW |"raw" |
| 整數 |OBJ_ENCODING_INT |"int" |
| embstr 編碼的簡單動態字串 |OBJ_ENCODING_EMBSTR |"embstr" |
| 字典 |OBJ_ENCODING_HT |"hashtable" |
| 壓縮列表 |OBJ_ENCODING_ZIPLIST |"ziplist" |
| 快速列表 |OBJ_ENCODING_QUICKLIST |"quicklist" |
| 整數集合 |OBJ_ENCODING_INTSET |"intset" |
| 跳躍表 |OBJ_ENCODING_SKIPLIST |"skiplist" |

總結來看,如下圖:

圖 5 - 11 種不同編碼的資料物件

十一種不同編碼的物件分別是:

  1. 使用雙端或快速列表實現的列表物件
  2. 使用壓縮列表實現的列表物件
  3. 使用字典實現的雜湊物件
  4. 使用壓縮列表實現的雜湊物件
  5. 使用字典實現的集合物件
  6. 使用整數集合實現的集合物件
  7. 使用壓縮列表實現的有序集合物件
  8. 使用跳躍表實現的有序集合物件
  9. 使用普通 SDS 實現的字串物件
  10. 使用 embstr 編碼的 SDS 實現的字串物件
  11. 使用整數值實現的字串物件

接下來,我們將對上述十一種物件一一介紹。之後再一一認識物件編碼。

2 字串物件

字串物件的可選編碼分別是:int、raw 或者 embstr。

2.1 int 編碼的字串物件

如果一個字串物件儲存的是整數值,並且這個整數值可以用 long 型別表示,那麼字串物件會將整數值儲存在字串物件結構的 ptr 屬性中,並將字串物件的編碼設定為 int。

我們執行以下 SET 命令,伺服器將建立一個如下圖所示的 int 編碼的字串物件作為 num 鍵的值:

# redis-cli
127.0.0.1:6380> set num 12345
OK
127.0.0.1:6380> OBJECT ENCODING num
"int"

圖 6 - int 編碼的字串物件

2.2 raw 編碼的字串物件

如果字串物件儲存的是一個字串值,並且這個字串值的長度大於 44 位元組(根據版本的不同,這個值會有差異。詳見 object.c 檔案中的 OBJ_ENCODING_EMBSTR_SIZE_LIMIT 常量),那麼字串物件將使用**簡單動態字串(SDS)來儲存這個字串值,並將物件的編碼設定為 raw。

我們執行下面的 SET 命令,伺服器將建立一個圖 7 所示的 raw 編碼的字串物件作為 k1 鍵的值(45 位元組):

127.0.0.1:7379> set story 'k01234567890123456789012345678901234567890123'
OK
127.0.0.1:7379> OBJECT ENCODING k4
"raw"

圖 7 - raw 編碼的字串物件

2.3 embstr 編碼的字串物件

如果字串儲存的是一個字串值,並且這個字串值的長度小於等於 44 位元組(根據版本的不同,這個值會有差異。詳見 object.c 檔案中的 OBJ_ENCODING_EMBSTR_SIZE_LIMIT 常量),那麼字串物件將使用 embstr 編碼的方式來儲存這個字串。

embstr 編碼是專門用於儲存段字串的一種優化編碼方式,這種編碼和 raw 編碼一樣,都使用 redisObject 和 sdshdr 結構來表示字串物件。但和 raw 編碼的字串物件不同的是:

  • raw 編碼會呼叫兩次記憶體分配函式來分別建立 redisObject 和 sdshdr 結構
  • embstr 編碼通過一次記憶體分配函式分配一塊連續的空間,空間中依次包含 redisObject 和 sdsHdr 兩個結構。

相對應的,釋放記憶體時,embstr 編碼的物件也只需呼叫一次記憶體釋放函式。

因此,使用 embstr 編碼的字串物件來儲存短字串值有以下好處:

  • 建立字串物件時,記憶體分配次數從兩次降低為一次。
  • 釋放 embstr 編碼的字串物件時,呼叫記憶體釋放函式的次數從兩次降低為一次。
  • 更好地利用快取優勢。embstr 編碼的字串物件的所有資料都儲存在一塊連續的記憶體中 ,這種方式比 raw 編碼的字串物件能夠更好的利用快取帶來的優勢。

以下命令建立了一個 embstr 編碼的字串物件作為 msg 鍵的值,值物件結構如圖 8。

127.0.0.1:6380> SET msg hello
OK
127.0.0.1:6380> OBJECT ENCODING msg
"embstr"

圖 8 - embstr 編碼的字串物件

2.4 浮點數編碼

Redis 中,long double 型別的浮點數也是作為字串值來儲存的。

我們要儲存一個浮點數到字串物件中,程式會先將這個浮點數轉換成字串值,然後再儲存轉換所得的字串值。

執行以下程式碼,將建立一個包含 3.14 的字串表示 "3.14" 的字串物件:

127.0.0.1:6380> SET pi 3.14
OK
127.0.0.1:6380> OBJECT ENCODING pi
"embstr"

在有需要的時候,程式會將儲存在字串物件裡的字串值轉換成浮點數值,執行某些操作,然後將所得的浮點數值轉換回字串值,繼續儲存在字串物件中。

比如,我們對 pi 鍵執行以下操作:

127.0.0.1:6380> INCRBYFLOAT pi 2.0
"5.14"
127.0.0.1:6380> OBJECT ENCODING pi
"embstr"

執行 INCRBYFLOAT 命令過程中,實際上就會出現字串與浮點數值互相轉換的情況。

2.5 編碼轉換

int 編碼的字串物件和 embstr 編碼的字串物件在滿足某些條件的情況下,會被轉換為 raw 編碼的字串物件。

對於 int 編碼的字串物件來說,如果我們在執行命令後,使得這個物件儲存的不再是整數值,而是一個字串,那麼字串物件就會從 int 變為 raw。比如 APPEND 命令等。

另外,對於 embstr 編碼的字串,由於 Redis 沒有為其編寫任何相應的修改程式,所以 embstr 編碼的字串物件實際上是隻讀的。當我們對 embstr 編碼的字串物件執行任何修改命令時,程式都會先將物件的編碼從 embstr 轉換成 raw。也就是說,embstr 編碼的字串一旦修改,一定會轉換成 raw 編碼的字串物件

2.6 值與編碼對應關係

對於字串物件各個編碼的情況,總結如下:
| 值 | 編碼|
| :-- | :-- |
| 可以用 long 表示的整數值 | int |
| 可以用 long double 儲存的浮點數 | raw 或 embstr |
| 不可以用 long 或 long double 表示的整數或小數值 | raw 或 embstr |
| 大於 44 位元組的字串 | raw |
| 小於或等於 44 位元組的字串 | embstr |

3 列表物件

列表物件的可選編碼分別是:quicklist(3.2 版本前是 ziplist 和 linkedlist)。

3.1 quicklist 編碼的列表物件

3.2 版本引入了 quicklist 編碼,此編碼結合了 ziplist 和 linkedlist,使用雙向連結串列的形式,在每個節點上儲存一個 ziplist,而每個 ziplist 又可以儲存多個鍵值對。也就是說,quicklist 每個節點上儲存的不是一個資料,而是一片資料。

執行以下命令,伺服器將會建立一個列表物件,quicklist 結構如圖 8 所示:

127.0.0.1:7379> RPUSH animal 'dog' 'cat' 'pig'
(integer) 3
(5.12s)
127.0.0.1:7379> OBJECT ENCODING animal
"quicklist"

圖 8 - quicklist 編碼的列表物件

總結

  1. Redis 自己實現了一套物件系統來實現所有功能。
  2. 物件有物件型別物件編碼
  3. 物件型別對應字串、列表、雜湊、集合、有序集合五種
  4. 物件編碼對應跳躍表、壓縮列表、集合、動態字串等八種底層資料結構

相關文章