Grape
所有視頻:https://segmentfault.com/a/11...segmentfault
intset是Redis中的一種數據結構,地位和ziplist,dict通常。數組
intset是Redis集合的底層實現之一,當添加的全部數據都是整數時,會使用intset;不然使用dict。特別的,當遇到添加數據爲字符串,即不能表示爲整數時,Redis 會把數據結構轉換爲 dict,即把 intset 中的數據所有搬遷到 dict。數據結構
intset將整數元素按順序存儲在數組裏,並經過二分法下降查找元素的時間複雜度。數據量大時,依賴於「查找」的命令(如SISMEMBER)就會因爲O(logn)的時間複雜度而遇到必定的瓶頸,因此數據量大時會用dict來代替intset。可是intset的優點就在於比dict更省內存,並且數據量小的時候O(logn)未必會慢於O(1)的hash function。這也是intset存在的緣由。app
首先,咱們看一下Intset的結構體:dom
typedef struct intset { uint32_t encoding; //intset的類型編碼 uint32_t length; //成員元素的個數 int8_t contents[]; //用來存儲成員的柔性數組 } intset;
而後對比dict的結構體:ide
typedef struct dict { dictEntry **table; dictType *type; unsigned long size; unsigned long sizemask; unsigned long used; void *privdata; } dict; //須要注意contents數組成員被聲明爲int8_t類型並不表示contents裏存的是int8_t類型的成員,這個類型聲明對於contents來講能夠 //認爲是毫無心義的,由於intset成員是什麼類型徹底取決於encoding變量的值。encoding提供下面三種值: /* Note that these encodings are ordered, so: * INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */ #define INTSET_ENC_INT16 (sizeof(int16_t)) #define INTSET_ENC_INT32 (sizeof(int32_t)) #define INTSET_ENC_INT64 (sizeof(int64_t)) //若是intset的encoding爲INTSET_ENC_INT16,則contents的每一個成員的「邏輯類型」都爲int16_t。
觀察intset和dict結構體的空間大小,結果顯而易見。在數據量小的時候O(logn)未必會慢於O(1)的hash,因此intset的存在也變得必要。
另外,咱們看intset的結構體,觀察源碼咱們知道encoding的種類只有三種,用uint32存儲好像有點浪費空間,那麼咱們在構建結構體的時候是否能夠再省一些空間呢?筆者簡單拋個磚,把結構體構形成如下這種結構體:函數
typedef struct intset { uint32_t length; uint8_t encoding; int8_t contents[]; } intset;
你們以爲是否能夠呢?ui
另外,在此處咱們要注意一個問題,intset無論在什麼機器上都按照同一種字節序(小端)在內存中存儲intset的成員變量。爲何呢?
若是老老實實經過contents[x]的方式賦值取值,咱們就不須要考慮這個字節序的問題,可是intset根據encoding的值指定元素的地址偏移,暴力地對內存進行操做。若數據被截斷了,則大端機器和小端機器會表現出不統一的情況。爲了不這種狀況發生,intset無論在什麼機器上都按照同一種字節序(小端)在內存中存intset的成員變量。this
那麼什麼狀況下會出現元素的地址偏移呢?不要着急,咱們在下文intset的操做的時候會看到,要注意觀察哦。編碼
首先咱們能夠從源碼中看到intset的一系列操做:
intset *intsetNew(void); intset *intsetAdd(intset *is, int64_t value, uint8_t *success); intset *intsetRemove(intset *is, int64_t value, int *success); uint8_t intsetFind(intset *is, int64_t value); int64_t intsetRandom(intset *is); uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value); uint32_t intsetLen(const intset *is); size_t intsetBlobLen(intset *is);
那麼限於篇幅,筆者就拿插入來具體分析,其餘若感興趣可自行查看源碼。
對於intset的插入有兩種狀況,分別爲:
若是是第一種狀況,若value的encoding大於要插入的intset的encoding,則調用intsetUpgradeAndAdd直接升級intset的encoding並插入到首部或者尾部。若value的encoding小於要插入的intset的encoding,則不須要升級intset的encoding,調用intsetSearch找到合適的插入位置,再將該位置到contents尾部的數據所有右移一格,最後將value插入到pos。
是的,很簡單,在插入元素的時候比較插入值的encoding和現有的encoding的值,若小於,本身查詢位置插入,不然就升級intset插入首部和尾部。對於查詢這塊,底層用的是二分查找,感興趣的讀者能夠去看一看,而爲何是插入首部和尾部,由於在擴展編碼以後可能插入的值爲負數。
插入的源代碼:
/* Insert an integer in the intset *///success傳null進來則說明外層調用者不須要知道是否插入成功(value是否已存在),不然success用於此目的 intset *intsetAdd(intset *is, int64_t value, uint8_t *success) { uint8_t valenc = _intsetValueEncoding(value);//根據value的大小計算value的encoding uint32_t pos; if (success) *success = 1; /* Upgrade encoding if necessary. If we need to upgrade, we know that * this value should be either appended (if > 0) or prepended (if < 0), * because it lies outside the range of existing values. */ if (valenc > intrev32ifbe(is->encoding)) { //這種插入須要改變encoding(不須要search,由於encoding改變說明value必定插入在contents首部或者尾部) /* This always succeeds, so we don't need to curry *success. */ return intsetUpgradeAndAdd(is,value); } else { /* Abort if the value is already present in the set. * This call will populate "pos" with the right position to insert * the value when it cannot be found. */ if (intsetSearch(is,value,&pos)) { if (success) *success = 0;//intset裏已存在該值,返回失敗 return is; } is = intsetResize(is,intrev32ifbe(is->length)+1); if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);//右移一格 } _intsetSet(is,pos,value);//插入值 is->length = intrev32ifbe(intrev32ifbe(is->length)+1); return is; } /* Return the required encoding for the provided value. *///根據v值的大小決定須要的編碼類型static uint8_t _intsetValueEncoding(int64_t v) { if (v < INT32_MIN || v > INT32_MAX) return INTSET_ENC_INT64; else if (v < INT16_MIN || v > INT16_MAX) return INTSET_ENC_INT32; else return INTSET_ENC_INT16; } /* Upgrades the intset to a larger encoding and inserts the given integer. *///這個函數執行的前提是value參數的大小超過了當前編碼//爲is->content從新分配內存並修改編碼添加value進這個intsetstatic intset *intsetUpgradeAndAdd(intset *is, int64_t value) { uint8_t curenc = intrev32ifbe(is->encoding);//當前編碼類型 uint8_t newenc = _intsetValueEncoding(value);//新的編碼類型 int length = intrev32ifbe(is->length); int prepend = value < 0 ? 1 : 0;//由於value必定超過了編碼的限制,因此看value是大於0仍是小於0以此決定value放置在content[0]仍是content[length] /* First set new encoding and resize */ is->encoding = intrev32ifbe(newenc); is = intsetResize(is,intrev32ifbe(is->length)+1); /* Upgrade back-to-front so we don't overwrite values. * Note that the "prepend" variable is used to make sure we have an empty * space at either the beginning or the end of the intset. */ while(length--) //以curenc爲編碼倒序取出全部值並賦值給新的位置 _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc)); /* Set the value at the beginning or the end. */ if (prepend) _intsetSet(is,0,value); else _intsetSet(is,intrev32ifbe(is->length),value); is->length = intrev32ifbe(intrev32ifbe(is->length)+1); return is; } /* Resize the intset *///解除is的內存分配並從新分配長度爲len的intset的內存static intset *intsetResize(intset *is, uint32_t len) { uint32_t size = len*intrev32ifbe(is->encoding); is = zrealloc(is,sizeof(intset)+size); return is; } //把from索引到intset尾部的整塊數據複製to索引(複製以後from值不變,可是能夠被覆蓋)static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) { void *src, *dst; uint32_t bytes = intrev32ifbe(is->length)-from; uint32_t encoding = intrev32ifbe(is->encoding); if (encoding == INTSET_ENC_INT64) { src = (int64_t*)is->contents+from; dst = (int64_t*)is->contents+to; bytes *= sizeof(int64_t); } else if (encoding == INTSET_ENC_INT32) { src = (int32_t*)is->contents+from; dst = (int32_t*)is->contents+to; bytes *= sizeof(int32_t); } else { src = (int16_t*)is->contents+from; dst = (int16_t*)is->contents+to; bytes *= sizeof(int16_t); } memmove(dst,src,bytes); }
經過intset底層實現咱們能夠發現:基於順序存儲的整數集合 執行一些須要用到查詢的命令時 其時間複雜度不會是文檔裏註明O(1),在操做一個成員插入,查詢的平均時間複雜度會是O(logn)。因此當整數集合數據量變大的時候,Redis會用dict做爲集合的底層實現,將SADD、SREM、SISMEMBER這些命令的時間複雜度降至O(1),固然,這會比intset消耗更多內存。因此Redis在實現的時候纔會在數據量小的時候採用intset。