面試官問我redis資料型別,我回答了8種

閃客sun發表於2020-11-11

面試官:小明呀,redis 有幾種資料結構呀?

小明:8 種

面試官:那你說一下分別是什麼?

小明:raw,int,ht,zipmap,linkedlist,ziplist,intset,skiplist,embstr

面試官:額,你在說什麼?

小明:在回答你的問題呀,這個問題我可是有過研究的,不會錯的

面試官:好吧,今天的面試先到這裡,你回去等通知吧

小明:...


上面發生的對話,到底是面試官有問題,還是小明有問題呢?其實是都有問題的,面試官的提問不準確,小明的回答也不準確。

但可以看出,面試官的水平一般,因為聽到這些名詞並不知道小明說的是 redis 底層的編碼型別,進而錯失了深入挖掘小明技術潛力的機會。而小明也有些自作聰明,忽略了面試官想考察的知識點,把自己最近看的一些皮毛拿出來秀了秀,結果導致了一場誤會。

就著上面這個引子,我們本篇文章就來聊聊,redis 中的資料結構那些事。

redis 原始碼選取的版本: 3.0.0

本篇文章的目標:知道 redis 的編碼型別這個概念,並按照原始碼級的深度去理解為什麼要設定不同的編碼型別,但不會過多展開各種底層資料結構的細節

redis 的物件型別與編碼型別

redis 的物件型別,就是面試中常考的 redis 資料型別有哪些 這個問題所問的準確說法,這個對於我們這些只會面試不會開發的程式設計師來說,簡直再熟悉不過啦,就是字串、雜湊、列表、集合、有序集合,這個在 redis 原始碼中能找到準確的定義:

redis.c

/* Object types */
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4

好多人對 redis 資料結構的理解可能就止步於此了,但其實這只是 redis 對外暴露的抽象結構,其底層實現要看其編碼型別來決定使用該編碼型別對應的資料結構。

如果一個物件型別只有一種底層資料結構的實現方式,那麼這個編碼型別就完全多餘了,早期的 redis 的確沒有這個概念。但後來為了優化效能,一種物件型別可能對應多種不同的編碼實現,於是乎關於 redis 底層資料結構的知識點,就開始複雜起來了。編碼型別在 redis 原始碼中也有準確定義:

redis.c

/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */
#define REDIS_ENCODING_RAW 0     /* Raw representation */
#define REDIS_ENCODING_INT 1     /* Encoded as integer */
#define REDIS_ENCODING_HT 2      /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6  /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define REDIS_ENCODING_EMBSTR 8  /* Embedded sds string encoding */

其實我們不用尋找任何額外的二手資料來解釋編碼型別的作用,直接看原始碼中的英文註釋即可。

物件編碼(編碼型別):有些物件型別如字串、雜湊,其內部實現可以有多種方式,一個 redis 物件的 encoding 欄位可以設定下面幾個值來表示這個物件的底層編碼型別

同一個物件型別,可以有不同的編碼型別作為底層實現。而同一種編碼型別,也可以支援上層的多種物件型別。他們的關係如下:

redis物件型別與編碼型別

讀到這裡你一定有至少三個疑問:

  • 為什麼一種物件型別要對應多種編碼型別,是為了解決什問題?
  • redis 怎麼知道什麼時候該用這種編碼型別,什麼時候該用那種編碼型別呢,並且編碼型別可以隨時改變麼?
  • 各種編碼型別的實現原理是什麼?(本章不做重點,會貫穿全文介紹一些基本思想,具體的各種實現會在其他篇章專門講解)

別急,這一部分只是讓你知道,redis 面對使用者暴露的只是一個抽象的資料結構,並不代表其底層的具體實現。接下來帶你慢慢深入。

為什麼一種物件型別要對應多種編碼型別

寫 redis 的大牛也是程式設計師,總不能他給自己增加了程式碼的複雜性,又對效能提升毫無幫助吧?畢竟 redis 這種中間元件必須以效能來取勝同類產品。沒錯,就是為了 效能提升

直觀感受編碼型別的不同

首先我們來直觀感受一下同一物件對應不同編碼型別這一場景,這裡用到了 object encoding xxx 這個 redis 命令來檢視某一個 key 其 value 物件所使用的編碼型別

127.0.0.1:6379> set number 100
OK
127.0.0.1:6379> object encoding number
"int"
127.0.0.1:6379> set number "100"
OK
127.0.0.1:6379> object encoding number
"int"
127.0.0.1:6379> set number abc
OK
127.0.0.1:6379> object encoding number
"embstr"
127.0.0.1:6379> set number aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
OK
127.0.0.1:6379> object encoding number
"raw"
127.0.0.1:6379> set number 9999999999999999999999999
OK
127.0.0.1:6379> object encoding number
"embstr"
127.0.0.1:6379> set number 99999999999999999999999999999999999999999999999999999999999999
OK
127.0.0.1:6379> object encoding number
"raw"

我們用我們最常使用的字串做了測試,觀察到其編碼型別隨著我設定的 value 值不同而改變,我整理了如下表格來表示上面的測試結果

value 編碼型別
100 int
"100" int
abc embstr
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa raw
9999999999999999999999999 embstr
99999999999999999999999999999999999999999 raw

當然,我是因為知道字串的編碼型別的條件,踩專門選取了這些有代表性的值進行測試,我們可以總結出一個規律

  • 不論是 100 還是 "100",編碼型別都是 int,說明 redis 在判斷是否可以用整數這個編碼型別表示物件的時候,就只是看這個值是否能轉換成一個整數
  • 比較短的字串 abc 被編碼為 embstr,比較長的字串 aaaaaaa..a 被編碼為 raw,說明長短字串的編碼型別不一樣,由此可以猜測 redis 可能是對短的字串進行了儲存上的優化策略(當然目前只是合理猜測,還有可能是對長字串進行了某種優化)
  • 整數 999...9 和更長的整數 9999999...9 也都被轉換成了相應的表示字串的型別,說明可以用整數編碼型別表示的值,是有一定大小限制的

redis 對字串編碼型別的優化淺析

上面的實驗我們瞭解到,字串物件的編碼型別確實有三種:int,raw,embstr。

int 型別分析起來沒什麼意思,想想就知道肯定是能用整型儲存的,儘量用整型儲存,一定比字串方式更節省空間嘛。下面我們分析一下,長字串和短字串的編碼型別做了區分,這是為什麼呢?

不只是字串型別,包括雜湊、列表這些物件型別,都是用一個統一的結構體 redisObject 來表示的。他的結構如下:

redis.h

typedef struct redisObject {
    unsigned type:4; // 物件型別
    unsigned encoding:4; // 編碼型別
    void *ptr; // 值的指標
    ...(省略一些不重要的欄位)
} robj;

佔了 4 位的 type 表示 物件型別(5 種那個),同樣佔了 4 位的 encoding 欄位表示 編碼型別(8 種那個),指標欄位 ptr 表示實際值的 記憶體地址

如果該物件的編碼型別為整數(encoding=REDIS_ENCODING_INT),那麼這個 ptr 指向的將會是一個 long 型別的變數。

util.c

if (!string2ll(s,slen,&llval))
    return 0;
...
*lval = (long)llval;
return 1;

object.c

...
o->ptr = (void*) value;

如果該物件的編碼型別為 raw 或者 embstr,那麼這個 ptr 指向的將會是一個 sdshdr 結構的變數

sds.h

struct sdshdr {
    unsigned int len; // 字串長度
    unsigned int free; // buf空閒數
    char buf[]; // 字元陣列
};

既然都是指向同一個結構,那是怎麼優化的呢?那就得進入如下兩個方法具體看看了

object.c

robj *createStringObject(char *ptr, size_t len) {
    if (len <= 39)
        return createEmbeddedStringObject(ptr,len);
    else
        return createRawStringObject(ptr,len);
}

你看,這段程式碼非常清晰,字串長度 <=39 時,就建立 embstr 型別的字串物件,否則建立 raw 型別的字串物件。那麼這兩個建立方式的區別,一定就隱藏在這兩個方法裡,我們點進去!

embstr 型別

robj *createEmbeddedStringObject(char *ptr, size_t len) {
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1);
    struct sdshdr *sh = (void*)(o+1);
    o->type = REDIS_STRING;
    o->encoding = REDIS_ENCODING_EMBSTR;
    o->ptr = sh+1;
    ... (一些賦值操作)
    return o;
}

raw 型別

robj *createRawStringObject(char *ptr, size_t len) {
    return createObject(REDIS_STRING,sdsnewlen(ptr,len));
}

sds sdsnewlen(const void *init, size_t initlen) {
    ...
    struct sdshdr *sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
    ...
}

robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));
    o->type = type;
    o->encoding = REDIS_ENCODING_RAW;
    o->ptr = ptr;
    ...(一些賦值操作)
    return o;
}

對於閱讀原始碼比較多的同學,可能立刻就察覺到了他們的區別。其實很簡單,就是 raw 型別這種方式,為 redisObject 和 sdshdr 結構分別申請了記憶體空間,而 embstr 只申請了一次記憶體空間,然後將這兩個結構緊挨著放。除此之外沒有其他任何區別了。直觀圖如下:

BjBxXj.png

BjBwpF.png

看到這,一切就都解釋通了,非常簡單,就只是申請記憶體這一步的區別而已。但對於我們這些什麼簡單的事情都要包裝成高階大氣話術的程式設計師來說,還是要想辦法裝一下,我們總結出使用 embstr 編碼相比於 raw 編碼的好處:

  • embstr 只申請了一次記憶體,而 raw 需要申請兩次,因此節約了一次申請記憶體的消耗
  • 釋放 embstr 只需要釋放一次記憶體,而 raw 需要兩次,因此節約了一次釋放記憶體的消耗
  • embstr 的 redisObject 和 sdshdr 放在一塊連續的記憶體裡,因此更能利用 快取 帶來的優勢

怎麼樣,原始碼級的理解,加上迷倒面試官的總結話術,夠意思吧。

不同編碼型別的條件

上個部分我們通過字串,觀察了不同的編碼型別,也理解了為什麼要有不同的編碼型別的實現。接下來我們總結下其他的物件與編碼型別,原理就不深入原始碼分析了,和字串的基本思想是一樣的。

字串的編碼型別

  • int:8 個位元組的長整型
  • embstr:小於等於 39 位元組的字串
  • raw:大於 39 位元組的字串

雜湊的編碼型別

  • ziplist:元素個數小於 512,且所有值都小於 64 位元組
  • hashtable:除上述條件外

列表的編碼型別

  • ziplist:元素個數小於 512,且所有值都小於 64 位元組
  • hashtable:除上述條件外

集合的編碼型別

  • intset:元素個數小於 512,且所有值都是整數
  • hashtable:除上述條件外

有序集合的編碼型別

  • ziplist:元素個數小於 128,且所有值都小於 64 位元組
  • hashtable:除上述條件外

由於不展開講解,純記憶的東西我覺得用最乾淨的辦法描述給大家即可,無多餘部分。具體資料結構的細節,我會用其他文章來講解


此時,經過一番修煉的小明,再次遇到了一位專業的面試官

專業的面試官:小明呀,redis 有幾種資料結...

進化的小明:面試官面試官,你這個問題分兩種情況,redis 的物件型別,也就是我們常說的對外暴露的資料型別,有 5 種,分別是.... 底層對應的編碼型別,在 3.0.0 原始碼中有 8 種,分別是....

專業的面試官:誰讓你搶答了?

進化的小明:...

專業的面試官:行了,今天的面試先到這裡,你回去等通知吧

進化的小明:...


相關文章