面試官:小明呀,redis 有幾種數據結構呀?程序員
小明:8 種面試
面試官:那你說一下分別是什麼?redis
小明:raw,int,ht,zipmap,linkedlist,ziplist,intset,skiplist,embstr數組
面試官:額,你在說什麼?緩存
小明:在回答你的問題呀,這個問題我但是有過研究的,不會錯的數據結構
面試官:好吧,今天的面試先到這裏,你回去等通知吧源碼分析
小明:...性能
上面發生的對話,究竟是面試官有問題,仍是小明有問題呢?實際上是都有問題的,面試官的提問不許確,小明的回答也不許確。測試
但能夠看出,面試官的水平通常,由於聽到這些名詞並不知道小明說的是 redis 底層的編碼類型,進而錯失了深刻挖掘小明技術潛力的機會。而小明也有些自做聰明,忽略了面試官想考察的知識點,把本身最近看的一些皮毛拿出來秀了秀,結果致使了一場誤會。優化
就着上面這個引子,咱們本篇文章就來聊聊,redis 中的數據結構那些事。
redis 源碼選取的版本: 3.0.0
本篇文章的目標:知道 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 這種中間組件必須以性能來取勝同類產品。沒錯,就是爲了 性能提高。
首先咱們來直觀感覺一下同一對象對應不一樣編碼類型這一場景,這裏用到了 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 |
固然,我是由於知道字符串的編碼類型的條件,踩專門選取了這些有表明性的值進行測試,咱們能夠總結出一個規律
上面的實驗咱們瞭解到,字符串對象的編碼類型確實有三種: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 只申請了一次內存空間,而後將這兩個結構緊挨着放。除此以外沒有其餘任何區別了。直觀圖以下:
看到這,一切就都解釋通了,很是簡單,就只是申請內存這一步的區別而已。但對於咱們這些什麼簡單的事情都要包裝成高端大氣話術的程序員來講,仍是要想辦法裝一下,咱們總結出使用 embstr 編碼相比於 raw 編碼的好處:
怎麼樣,源碼級的理解,加上迷倒面試官的總結話術,夠意思吧。
上個部分咱們經過字符串,觀察了不一樣的編碼類型,也理解了爲何要有不一樣的編碼類型的實現。接下來咱們總結下其餘的對象與編碼類型,原理就不深刻源碼分析了,和字符串的基本思想是同樣的。
因爲不展開講解,純記憶的東西我以爲用最乾淨的辦法描述給你們便可,無多餘部分。具體數據結構的細節,我會用其餘文章來說解。
此時,通過一番修煉的小明,再次遇到了一位專業的面試官
專業的面試官:小明呀,redis 有幾種數據結...
進化的小明:面試官面試官,你這個問題分兩種狀況,redis 的對象類型,也就是咱們常說的對外暴露的數據類型,有 5 種,分別是.... 底層對應的編碼類型,在 3.0.0 源碼中有 8 種,分別是....
專業的面試官:誰讓你搶答了?
進化的小明:...
專業的面試官:行了,今天的面試先到這裏,你回去等通知吧
進化的小明:...
完