Redis源碼閱讀【6-整數集合】

Redis源碼閱讀【1-簡單動態字符串】
Redis源碼閱讀【2-跳躍表】
Redis源碼閱讀【3-Redis編譯與GDB調試】
Redis源碼閱讀【4-壓縮列表】
Redis源碼閱讀【5-字典】
Redis源碼閱讀【6-整數集合】
Redis源碼閱讀【7-quicklist】
Redis源碼閱讀【8-命令處理生命週期-1】
Redis源碼閱讀【8-命令處理生命週期-2】
Redis源碼閱讀【8-命令處理生命週期-3】
Redis源碼閱讀【8-命令處理生命週期-4】
Redis源碼閱讀【番外篇-Redis的多線程】
建議搭配源碼閱讀源碼地址

1、介紹

整數集合(intset)是一個有序,存儲整型數據的結構。我們知道Redis是一個內存數據庫,所以必須考慮如何才能高效的利用內存,使用傳統字符串的方式來存儲整型,無疑是浪費內存空間,當Redis集合類型的元素都是整數並且都處在64位有符號整數範圍之內時,使用該結構體存儲。我們先執行一段代碼如下所示:
在這裏插入圖片描述

當我們添加元素 1 2 -1 -6 的時候,底層的存儲結構是 intset 但是當我, 再添加元素 ‘a’ 的時候底層元素變成了 hashtable

2、數據存儲

整數集合在Redis中可以保存int16_tint32_tint64_t類型的整型數據,並且可以保證集合中不會出現重複數據。每個整數集合使用一個intset類型的數據結構表示。intset結構如下

typedef struct intset {
    uint32_t encoding; //編碼類型
    uint32_t length;// 元素個數
    int8_t contents[]; // 柔性數組,根據encoding字段決定幾個字節表示一個元素
} intset;

在這裏插入圖片描述
encoding:編碼類型,決定每個元素佔用幾個字節。有如下三種類型:

INTSET_ENC_INT16:當元素值都位於INT16_MIN 和 INT16_MAX 之間時使用。該編碼方式爲每個元素佔用2個字節
INTSET_ENC_INT32:當元素值位於 INT16_MAX 到 INT32_MAX 或者INT32_MIN 到 INT16_MIN 之間使用。該編碼方式爲每個元素佔用4個字節。
INTSET_ENC_INT64:當元素值位於INT32_MAX 到 INT64_MAX 或者 INT64_MIN 到 INT32_MIN 之間時使用。該編碼方式爲每個元素佔用8個字節

編碼
INTSET_ENC_INT16 (2147483647,9223372036854775807)或[-9223372036854775808,-2147483648)
INTSET_ENC_INT32 (32767,2147483647) 或 [-2147483648,-32768)
INTSET_ENC_INT64 [-32768,32767]

intset 結構體會根據待插入的值決定是否需要進行擴容操作。擴容會修改encoding字段,而encoding字段決定了一個元素在contents柔性數組中佔用幾個字節。所以當修改encoding字段之後,intset中原來的元素也需要在contents中進行相應的擴展。注意這裏就有一個理論,只要待插入的值導致擴容,則該值在待插入的intset中不是最大值,就是最小值
encoding 字段在Redis中使用宏來表示。其定義如下:

//整數集合定義宏
#define INTSET_ENC_INT16 (sizeof(int16_t)) // int 16 2字節
#define INTSET_ENC_INT32 (sizeof(int32_t)) // int 32 4字節
#define INTSET_ENC_INT64 (sizeof(int64_t)) // int 64 8字節

因爲encoding字段實際取值爲 2、4、8,所以encoding字段可以直接比較大小。當待插入值的encoding 字段 大於待插入intset 的 encoding時,說明需要進行擴容操作,並且也能表明該待插入值在該intset中肯定不存在。

length:元素個數。即一個intset中包括多少個元素。
contents:存儲具體元素。根據 encoding 字段決定多少個字節表示一個元素。
按此存儲結構,上文示例中生成的testSet存儲內容如圖所示:
在這裏插入圖片描述

encoding字段爲2,代表INTSET_ENC_INT16。length 字段爲 4,代表該intset中有4個元素。根據encoding字段,每個元素分別佔用兩個字節,並且按從小到大的順序排列,依次爲-6、-1、1、2

3、基本操作

下面將介紹一下intset中查詢,添加,刪除 元素操作,以及介紹一下其常用的API

3.1、查詢元素

查詢元素的入口函數是intsetFind,該函數首先進行一些防禦性判斷,如果沒有通過判斷則直接返回。intset是按從小到大有序排列的,所以通過防禦性判斷之後使用二分進行元素查找(因爲是有序的)。一下是Redis中intset查詢代碼的實現:

//intset查詢
uint8_t intsetFind(intset *is, int64_t value) {
    uint8_t valenc = _intsetValueEncoding(value); //判斷編碼方式
    return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL); 
    //編碼方式如果大於當前intset的編碼方式,直接返回0。否則調用intsetSearch函數進行查找
}


//intset具體查詢
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
    int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
    int64_t cur = -1;
    
    //如果intset沒有元素,直接返回0
    if (intrev32ifbe(is->length) == 0) {
        if (pos) *pos = 0;
        return 0;
    } else {
        //如果元素大於最大值或者最小值,直接返回
        if (value > _intsetGet(is,max)) {
            if (pos) *pos = intrev32ifbe(is->length);
            return 0;
        } else if (value < _intsetGet(is,0)) {
            if (pos) *pos = 0;
            return 0;
        }
    }
    //二分查找
    while(max >= min) {
        //這裏使用了位運算 同時使用了unsigned c語言和 java 不太一樣 >> 1 代表除於二 
        mid = ((unsigned int)min + (unsigned int)max) >> 1;
        cur = _intsetGet(is,mid);
        if (value > cur) {
            min = mid+1;
        } else if (value < cur) {
            max = mid-1;
        } else {
            break;
        }
    }
    
    //判斷元素是否找到找到返回1 未找到返回0
    if (value == cur) {
        if (pos) *pos = mid;
        return 1;
    } else {
        if (pos) *pos = min;
        return 0;
    }
}

intsetSearch 就是查找的核心函數,其本質就是使用了二分查找法,具體流程如下:

1、函數定義uint8_t intsetFind(intset *is,int64_t value)。第一給參數爲待查詢的intset,第2個參數爲待查找的值。首先判斷待查找的值需要的編碼格式(如上表中所示),如果編碼大於該intset的編碼,則值肯定不再其中,直接返回,否則調用intsetSearch 函數繼續查找。

2、intsetFind 繼續內部調用 intsetSearch ,先判斷intset中是否有值,如果當前intset爲空,則直接返回0。如果有值,判斷當前目標值是否介於intset的最大和最小值直接,如果否也直接返回0。
3、因爲intset是一個有序數組,則直接使用 二分查找 來查詢目標元素,找到返回1 否則返回0

3.2、添加元素

添加元素的入口函數是intsetAdd,該函數根據插入值的編碼類型和當前intset 的編碼類型決定是直接插入還是進行 intset 升級擴展後再執行插入(升級插入的函數入口爲intsetUpgradeAndAdd)如下是代碼實現:

//intset添加元素
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    //獲取目標元素的編碼
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    if (success) *success = 1;
    
    //如果大於當前intset的編碼則,說明要進行升級
    if (valenc > intrev32ifbe(is->encoding)) {
        /* This always succeeds, so we don't need to curry *success. */
        //調用升級函數
        return intsetUpgradeAndAdd(is,value);
    } else {
        //否則先進行查重,如果已經存在該元素,直接返回
        if (intsetSearch(is,value,&pos)) {
            if (success) *success = 0;
            return is;
        }
        //如果元素不存在,則添加元素
        is = intsetResize(is,intrev32ifbe(is->length)+1);//首先將intset佔用內存擴容
        //如果插入元素再intset中間位置,調用intsetMoveTail給元素挪出空間
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
    
    _intsetSet(is,pos,value);// 保存元素
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1); // 修改intset的長度,將其加1
    return is;
}

添加元素的流程如下:

1、函數定義爲 intset * intsetAdd(intset *is,int64_t value,unint8_t *success);第1個參數爲待添加元素的 intset,第2個參數 爲待插入的值,第3個參數 選傳,如果該元素已經在集合中存在,將success 置0
2、判斷要插入的值需要什麼編碼格式。如果當前intset 的編碼格式小於待插入的值需要的編碼格式,則調用 intsetUpgradeAndAdd 進行升級 (調用intsetUpgradeAndAdd會改變encoding)
3、調用intsetSearch 函數進行查重,即插入的值是否在當前集合中,如果找到了就不能再次插入,直接返回。如果沒有找到,在intsetSearch 中會將待插入值需要插入的位置賦值給 position。position 的計算邏輯也比較簡單,首先如果 intset 爲空,則需要將待插入值置於該 intset 的第一個位置,即 position 爲0,如果待插入的值小於 intset 最小值,position 也爲0 ,如果待插入值大於intset 的最大值,待插入值需要放到 intset 最後一個位置,即 position 爲 intset 的長度,如果上述幾種情況都不滿足,position 爲該 intset 中待插入值小於的第一個數之前的位置。
4、調用intsetResize擴充當前的intset,即給新插入的值申請好存儲空間。假設原來的元素個數爲length,編碼方式爲 encoding,則intsetResize會重新分配一塊內存,大小爲:encoding * (length + 1)
5、如果要插入的位置於原來元素之間,則調用intsetMoveTailposition 開始的數據移動到 position +1 的位置。
6、插入新值並將 intset 的長度字段 length 加1。

其中intsetMoveTail的操作如下圖:
在這裏插入圖片描述

那麼如果intset需要升級整個流程是怎麼樣的呢?

intsetAdd函數判斷當前編碼類型不能存放需要添加的新值時候,會調用intsetUpgradeAndAdd函數先升級當前的編碼類型。並且按新編碼類型重新存儲現有數據,然後將新的元素添加進去。

代碼如下:

static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    uint8_t curenc = intrev32ifbe(is->encoding); // 獲取當前編碼類型
    uint8_t newenc = _intsetValueEncoding(value); // 確定編碼類型
    int length = intrev32ifbe(is->length);
    //如果待插入元素小於0,說明需要插入到intset的頭部位置。如果大於0,需要插到intset的末尾位置。
    //因爲既然需要擴容了,那麼待插入的元素,不是最大值就是最小值
    int prepend = value < 0 ? 1 : 0;
    
    is->encoding = intrev32ifbe(newenc); 
    is = intsetResize(is,intrev32ifbe(is->length)+1);

    //從最後一個元素逐個往前擴容。從最後一個開始,這樣就不會出現覆蓋的情況
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

    //如果待插入元素小於0,插入intset頭部位置
    if (prepend)
        _intsetSet(is,0,value);
    else //否則插入到尾部
        _intsetSet(is,intrev32ifbe(is->length),value);
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

Redis intset升級並添加元素的過程如下:

1、函數定義爲 static intset *intsetUpgradeAndAdd(intset *is,int64_t value)
2、根據新的編碼方式調用intsetResize 重新申請空間。假設新編碼方式爲encodingNew,現有元素個數爲length,則需要的空間爲 encodingNew * (length +1)
3、移動並擴容原來的元素。注意擴容原來的元素時,按照從後往前的順序依次擴容,這樣可以避免數據被覆蓋。如果該值是正數則該值是最大值放在最後一位,如果是負數,則該值是最小值,放在第一位。
4、根據新插入是正數還是負數,將值插入相應的位置。
5、插入新值並將intset的長度字段length加1。

到這裏這個添加流程就結束了。

3.3、刪除元素

intset刪除元素的入口函數是intsetRemove,該函數查找需要刪除的元素然後通過內存地址的移動直接將該元素覆蓋。代碼如下:

intset *intsetRemove(intset *is, int64_t value, int *success) {
    uint8_t valenc = _intsetValueEncoding(value);//獲取待刪除元素的編碼
    uint32_t pos;
    if (success) *success = 0;
    //待刪除元素編碼必須小於等於intset編碼並且查找到該元素,纔會執行刪除操作
    if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
        uint32_t len = intrev32ifbe(is->length);

        if (success) *success = 1;
        //如果待刪除元素位於中間位置,則調用intsetMoveTail直接覆蓋掉該元素
        //如果待刪除元素位於intset末尾,則intset收縮內存後直接將其丟棄
        if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
        is = intsetResize(is,len-1);
        is->length = intrev32ifbe(len-1);//修改intset的長度,將其減1
    }
    return is;
}

刪除流程整體如下:

1、函數定義爲intset *intsetRemove(initset *is,int64_t value,int *success),首先判斷編碼是否小於等於當前編碼,若不是直接返回。
2、調用intsetSearch查找該值是否存在,不存在直接返回,存在則獲取該值所在位置 position
3、如果要刪除的數據不是該 intset 的最後一個值,則通過將 position+1 和之後位置的數據移動到 position 來覆蓋掉 position 位置的值。(如intsetMoveTail操作所示)

至此,intset的刪除操作全部完成了。

4、其它API

函數定義 實現原理 函數返回值
intset *intsetNew(void); 初始化一個intset,編碼爲INTSET_ENC_INT16,長度爲0.content未分配空間 intset指針
intset *intsetAdd(intset *is, int64_t value, uint8_t *success); 在intset中插入指定的值。若success不爲NULL,成功時置爲1,若待插入的值已存在則將 success 置爲0 intset指針
intset *intsetRemove(intset *is, int64_t value, int *success); 在intset中刪除指定的值。若success不爲NULL,成功時置爲1,若待插入的值已存在,則將success置爲0 intset指針
uint8_t intsetFind(intset *is, int64_t value); 在intset中查找值是否存在 成功返回 是否返回0
int64_t intsetRandom(intset *is); 在intset中隨機返回一個元素 隨機返回其中一個元素值
uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value); 在intset中獲取指定位置處的值。將獲取的值放入value中獲取intset的長度。通過intset結構體的length字段獲得 intset的長度
uint32_t intsetLen(const intset *is); 和intsetGet一樣 intset的長度
size_t intsetBlobLen(intset *is); 獲取intset總共佔用的字節數。計算方法爲length * encoding + sizeof(intset),即元素個數乘以元素編碼方式 + intset 結構體本身佔用的字節數 intset 共佔用的字節數
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章