【Redis5源碼學習】2019-04-18 整數集合intset


Grape
所有視頻:https://segmentfault.com/a/11...segmentfault


intset是Redis中的一種數據結構,地位和ziplist,dict通常。數組

intset的定義?

intset是Redis集合的底層實現之一,當添加的全部數據都是整數時,會使用intset;不然使用dict。特別的,當遇到添加數據爲字符串,即不能表示爲整數時,Redis 會把數據結構轉換爲 dict,即把 intset 中的數據所有搬遷到 dict。數據結構

intset存在的意義?

intset將整數元素按順序存儲在數組裏,並經過二分法下降查找元素的時間複雜度。數據量大時,依賴於「查找」的命令(如SISMEMBER)就會因爲O(logn)的時間複雜度而遇到必定的瓶頸,因此數據量大時會用dict來代替intset。可是intset的優點就在於比dict更省內存,並且數據量小的時候O(logn)未必會慢於O(1)的hash function。這也是intset存在的緣由。app

intset爲何更省內存?

首先,咱們看一下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的操做的時候會看到,要注意觀察哦。編碼

inset的騷操做?

首先咱們能夠從源碼中看到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的插入有兩種狀況,分別爲:

  1. 插入的value的encoding大於要插入的intset的encoding
  2. 插入的value的encoding小於要插入的intset的encoding

    若是是第一種狀況,若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。

相關文章
相關標籤/搜索