1. 程式人生 > >Redis5.0原始碼解析(五)----------整數集合

Redis5.0原始碼解析(五)----------整數集合

基於redis5.0

整數集合(intset)是集合鍵的底層實現之一: 當一個集合只包含整數值元素, 並且這個集合的元素數量不多時, Redis 就會使用整數集合作為集合鍵的底層實現

redis> SADD numbers 1 3 5 7 9
(integer) 5

redis> OBJECT ENCODING numbers
"intset"

整數集合的實現

整數集合(intset)是 Redis 用於儲存整數值的集合抽象資料結構, 它可以儲存型別為 int16_tint32_t 或者 int64_t 的整數值, 並且保證集合中不會出現重複元素

// intset.h

typedef struct intset {

	//決定contents陣列中儲存的型別
    uint32_t encoding;
    
	// 集合包含的元素數量
    uint32_t length;
    
    // 儲存元素的陣列
    int8_t contents[];
} intset;

雖然 intset 結構將 contents 屬性宣告為 int8_t 型別的陣列, 但實際上 contents 陣列並不儲存任何 int8_t 型別的值 —— contents 陣列的真正型別取決於 encoding 屬性的值:

  • 如果 encoding
    屬性的值為 INTSET_ENC_INT16 , 那麼 contents 就是一個 int16_t 型別的陣列
  • 如果 encoding 屬性的值為 INTSET_ENC_INT32 , 那麼 contents 就是一個 int32_t 型別的陣列
  • 如果 encoding 屬性的值為 INTSET_ENC_INT64 , 那麼 contents 就是一個 int64_t 型別的陣列

升級

每當我們要將一個新元素新增到整數集合裡面, 並且新元素的型別比整數集合現有所有元素的型別都要長時, 整數集合需要先進行升級(upgrade), 然後才能將新元素新增到整數集合裡面

假設現在有一個 INTSET_ENC_INT16

編碼的整數集合, 集合中包含三個 int16_t 型別的元素, 如圖 6-3 所示
在這裡插入圖片描述
每個元素都佔用 16 位空間, 所以整數集合底層陣列的大小為 3 * 16 = 48 位, 圖 6-4 展示了整數集合的三個元素在這 48 位裡的位置
在這裡插入圖片描述

假設我們要將型別為 int32_t 的整數值 65535 新增到整數集合裡面, 因為 65535 的型別 int32_t 比整數集合當前所有元素的型別都要長, 所以在將 65535 新增到整數集合之前, 程式需要先對整數集合進行升級

升級首先要做的是, 根據新型別的長度, 以及集合元素的數量(包括要新增的新元素在內), 對底層陣列進行空間重分配
在這裡插入圖片描述

升級完後各個元素的位置:
在這裡插入圖片描述

程式將整數集合 encoding 屬性的值從 INTSET_ENC_INT16 改為 INTSET_ENC_INT32 , 並將 length 屬性的值從 3 改為 4 , 設定完成之後的整數集合如圖 6-10 所示。
在這裡插入圖片描述

升級之後新元素的擺放位置
因為引發升級的新元素的長度總是比整數集合現有所有元素的長度都大, 所以這個新元素的值要麼就大於所有現有元素, 要麼就小於所有現有元素:

  • 在新元素小於所有現有元素的情況下, 新元素會被放置在底層陣列的最開頭(索引 0 );
  • 在新元素大於所有現有元素的情況下, 新元素會被放置在底層陣列的最末尾(索引 length-1 )。

升級的好處

提升靈活性

我們一般只使用 int16_t 型別的陣列來儲存 int16_t 型別的值, 只使用 int32_t 型別的陣列來儲存 int32_t 型別的值, 諸如此類。

但是, 因為整數集合可以通過自動升級底層陣列來適應新元素, 所以我們可以隨意地將 int16_tint32_t 或者 int64_t 型別的整數新增到集合中, 而不必擔心出現型別錯誤

節約記憶體

整數集合現在的做法既可以讓集合能同時儲存三種不同型別的值, 又可以確保升級操作只會在有需要的時候進行, 這可以儘量節省記憶體。

比如說, 如果我們一直只向整數集合新增 int16_t 型別的值, 那麼整數集合的底層實現就會一直是 int16_t 型別的陣列, 只有在我們要將 int32_t 型別或者 int64_t 型別的值新增到集合時, 程式才會對陣列進行升級

整數集合不支援降級操作, 一旦對陣列進行了升級, 編碼就會一直保持升級後的狀態

intset的API實現

建立一個空的整數集合:

/* Create an empty intset. */
intset *intsetNew(void) {
    intset *is = zmalloc(sizeof(intset));
    is->encoding = intrev32ifbe(INTSET_ENC_INT16);
    is->length = 0;
    return is;
}

向整數集合中新增元素:

/* Insert an integer in the intset */
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    uint8_t valenc = _intsetValueEncoding(value);
    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)) {
        /* 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;
            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;
}

從整數集合刪除一個元素:

/* Delete integer from intset */
intset *intsetRemove(intset *is, int64_t value, int *success) {
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    if (success) *success = 0;

    if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
        uint32_t len = intrev32ifbe(is->length);

        /* We know we can delete */
        if (success) *success = 1;

        /* Overwrite value with tail and update length */
        if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
        is = intsetResize(is,len-1);
        is->length = intrev32ifbe(len-1);
    }
    return is;
}