跟着大彬讀源碼 - Redis 5 - 對象和數據類型(上)

相信不少人應該都知道 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,爲何還要搞個編碼呢?ui

想一想看,經過 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. 對象編碼對應跳躍表、壓縮列表、集合、動態字符串等八種底層數據結構
相關文章
相關標籤/搜索