1. 程式人生 > >Redis內部資料結構詳解之整數集合(intset)

Redis內部資料結構詳解之整數集合(intset)

整數集合簡介

整數集合intset用於有序、無重複地儲存多個整數值,根據集合中元素的值自動選擇使用整數型別來儲存元素,例如:如果intset中絕對值最大的整數可以用int32_t來儲存,那麼整個intset中所有元素都使用int32_t來儲存。

如果當前intset所使用的型別不能儲存一個即將加入到該intset的新元素時候,需要對intset進行升級,比如新元素的型別是int64_t,而當前intset的型別是int32_t,那麼升級就是先將intset中所有元素由int32_t轉換為int64_t,然後再插入新元素。

對於int8_t,int32_t,int64_t我個人的理解就應該分別對應char,int,long long,使用int8_t,int32_t,int64_t應該是為了區分平臺的差異吧,具體的可以檢視stdint.h檔案。

整數集合的資料結構

typedef struct intset {
    uint32_t encoding; //所使用型別的長度,4\8\16
    uint32_t length; //元素個數
    int8_t contents[]; //儲存元素的陣列
} intset;

encoding的值是下面三個常量中的一個:

#define INTSET_ENC_INT16 (sizeof(int16_t))

#define INTSET_ENC_INT32 (sizeof(int32_t))

#define INTSET_ENC_INT64 (sizeof(int64_t))

contents陣列用來實際儲存資料,陣列中元素的特性:無重複元素;元素在陣列中遞增排列。

整數集合相關API介紹

函式名稱

作用

複雜度

_intsetValueEncoding

獲取給定整數的編碼型別

O(1)

_intsetGet

根據索引獲取整數值

O(1)

_intsetSet

根據索引設定給定整數值

O(1)

intsetNew

新建intset

O(1)

intsetResize

為給定的intset重新分配記憶體

O(1)

intsetSearch

查詢給定的整數是否在intset中

O(logN)

intsetUpgradeAndAdd

先升級intset然後插入元素

O(N)

intsetAdd

直接新增元素

O(N)

intsetMoveTail

將intset中元素偏移

O(N)

intsetRemove

刪除元素

O(N)

intsetRandom

隨機返回一個intset中元素

O(1)

intsetLen

intset中元素的個數

O(1)

intsetBlobLen

intset所佔的位元組數

O(1)

重要API原始碼的簡單解析

intsetAdd

//新增一個整數
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);
        //將從pos位置後面的值全部向後偏移一個位置,為新元素空出位置
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
    _intsetSet(is,pos,value);//新增新元素
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

intsetAdd函式新增一個元素value時,首先根據value的位元組數與當前intset的encoding進行比較,分析intset是否需要升級,若需要升級則呼叫intsetUpdateAndAdd函式處理,否則如果value已存在intset中直接pass,不存在,那麼先resize,接著將插入位置之後的所有元素向後偏移,新增value。

intsetMoveTail

/**使用memmove對集合進行向後偏移,下標從0開始,並且已經Resize
例:前 | 1 | 2 | 3 | 4 | 5 | 6 |   |   |
    from = 1, to = 3
    length = 6
    src = | 2 | 3 | 4 | 5 | 6 |
    dst = | 4 | 5 | 6 |   |   |
    bytes = 5 * sizeof(...)
   後 | 1 | 2 | 3 | 2 | 3 | 4 | 5 | 6 |
   偏移之前肯定需要用intsetResize函式,進行擴容,增加兩個容量
   如果不理解前後的變化,建議檢視memmove原始碼,這裡需要考慮到記憶體覆蓋的問題
   也就是為什麼必須使用memmove而不能使用memcpy的原因
*/
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);
}

intsetUpdateAndAdd

//對編碼型別進行升級,O(n)
//需要插入的值,要麼比當前集合中的最大值大,要麼比集合中的最小值小,不然不需要升級
//比最大值大還是小,只需要根據value的正負即可判斷
static 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;//決定新的值插入的位置(1表示頭,0表示尾)
    /* First set new encoding and resize */
    is->encoding = intrev32ifbe(newenc); //設定編碼型別
    is = intsetResize(is,intrev32ifbe(is->length)+1);//resize

    /* 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. */
    //通過_intsetGetEncoded得到升級前的該位置的整數值
    //設定原來的整數集的值,如果prepend=1表示新值在頭插入,那麼原來的數值全部向後偏移
    while(length--)
        _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;
}

intsetRemove

//刪除一個整數
intset *intsetRemove(intset *is, int64_t value, int *success) {
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    if (success) *success = 0;
    //value在原集合中
    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 */
        //如果 pos 不是 is 的最末尾,直接通過memmove記憶體覆蓋的方式刪除該整數值
        //如果是末尾,直接resize刪除
        if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
        is = intsetResize(is,len-1);//將空間縮小
        is->length = intrev32ifbe(len-1);
    }
    return is;
}

intset新增元素流程圖



小結

intset用於有序、無重複地儲存多個整數值,它會根據元素的值,自動選擇該用什麼長度的整數型別來儲存元素;

當新增新元素時,需要判斷當前intset的編碼型別能否儲存新元素,如果不行需要對intset進行升級,升級後的intset中的元素會擴大其佔有的位元組數,但是值不發生改變;

intset只支援升級,不支援降級,因此相對而言會浪費記憶體;

intset中元素是有序排列的,因此使用折半查詢的時間複雜度為O(logN)。

最後感謝黃健巨集(huangz1990)的Redis設計與實現及其他對Redis2.6原始碼的相關注釋對我在研究Redis2.8原始碼方面的幫助。