跟着大彬讀源碼 - Redis 10 - 對象編碼之整數集合

整數集合是 Redis 集合鍵的底層實現之一。當一個集合只包含整數值元素,而且元素數量很少時,Redis 就會使用整數集合做爲集合鍵的底層實現。數組

1 整數集合的實現

整數集合是 Redis 用於保存整數值的集合抽象數據結構。它能夠保存類型爲 int16_t、int32_t、int64_t 的整數值,而且保證集合中不會出現重複元素。數據結構

每一個 intset.h/intset 結構表示一個整數集合:ui

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;
  • encding:編碼方式
  • length:集合包含的元素數量
  • contents[]:保存元素的數組

contents 數組是整數集合的底層實現:整數集合的每一個元素都是 contents 數組的一個數組項,各個項在數組中按值的大小從小到大有序排列,而且數組中不包含重複項。編碼

length 屬性記錄了整數集合記錄的元素數量,也就是 contents 數組的長度。spa

雖然 intset 結構將 contents 屬性聲明爲 int8_t 類型的數組,但實際上 contents 數組並不保存任何 int8_t 類型的值,contents 數組的真正類型取決於 encoding 屬性的值,好比:若是 encoding 屬性的值爲 INTSET_ENC_INT16,那麼 contents 就是一個 int16_t 類型的數組,數組裏的每一個項都是一個 int16_t 類型的整數值,取值範圍爲:[-32768-32767](2^(16-1))。code

與之相似,encoding 的值爲 INTSET_ENC_INT32,那麼數組每項的取值範圍爲:[-2147483648, 2147483647](2^(32-1)。排序

這裏也引起了一個問題,當咱們對一個 encoding 爲 INTSET_ENC_INT8 的 intset,插入 129 時(int8_t 的取值範圍是 [-128, 127]),會出現什麼?索引

這也就引起了 intset 的升級操做。與之對應,也有降級操做。接下來,咱們來詳細認識下 intset 的升降級操做。內存

2 升級操做

每當咱們要將一個新元素添加到整數集合時,若是新元素的類型比整數集合的 encoding 類型大,整數集合就須要先進行升級操做(upgrade),而後才能將新元素添加到整數集合中。

整個升級操做源碼以下:

// intset.c/intsetUpgradeAndAdd()
/* Upgrades the intset to a larger encoding and inserts the given integer. */
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;

    /* 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--)
        _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;
}

升級整數集合並添加新元素,共分爲三步進行:

  1. 擴展底層數組大小。根據新元素的類型,擴展整數集合底層數組的大小,併爲新元素分配空間。
  2. 元素轉換,並保持原有順序。將底層數組現有的全部元素,都轉換成與新元素相同的類型,並將轉換後的元素放在正確的位置上,保證原有順序不發生改變。
  3. 將新元素添加到底層數組中。

此外,一旦因插入新元素引起升級操做,就說明新插入的元素比集合中現有的全部元素的長度大,因此這個新元素的值要麼大於全部現有元素(正值),要麼就小於全部現有元素(負值),那麼:

  • 在新元素小於全部現有元素時,新元素就會被放在底層數組的最開頭的位置,即索引爲 0 的位置;
  • 在新元素大於全部現有元素時,新元素就會被放在底層數組的最末尾的位置;

3 升級優點

整數集合的升級策略主要有如下兩個好處:

  1. 提示整數集合的靈活性;
  2. 儘量的節約內存;

3.1 提示靈活性

由於 C 語言是靜態類型語言,爲了不類型錯誤,咱們一般不會將兩種不一樣類型的值放在同一個數據結構中。

可是,由於有了升級操做,整數集合能夠經過它來自適應新元素,因此咱們能夠隨意地將 int16_t、int32_t、和 int64_t 類型的整數添加到集合中,而沒必要擔憂出現類型錯誤,大大的提高了整數集合的靈活性。

3.2 節約內存

固然,要讓一個數組能夠同時保存 int16_t、int32_t、和 int64_t 類型的整數值,咱們能夠粗暴的直接使用 int64_t 類型的數組做爲整數集合的底層實現,來保存不一樣類型的值。可是,這樣一來,即便添加到集合中的都是 int16_t、int32_t 類型的值,數組也都是須要使用 int64_t 類型的空間去保存,出現浪費內存的狀況。

而整數集合的升級操做,既能同時保存三種不一樣類型的值,又能夠確保升級操做只會在有須要的時候進行,達到節省內存的目的。

4 交、並、差集算法

Redis 中的集合實現了交、並、差等操做,相關操做可參加 t_set.c,其中
sinterGenericCommand() 實現交集,sunionDiffGenericCommand() 實現並集和差集。

它們都能同時對多個集合進行元素。當對多個集合進行差集運算時,會先計算出第一個和第二個集合的差值,而後再與第三個集合作差集,依次類推。

接下來,咱們一塊兒來認識下三個操做的實現思路。

4.1 交集

計算交集的過程大概能夠分爲三部分:

  1. 檢查各個集合,對於不存在的集合當作空集來處理。一旦出現空集,則不用繼續計算了,最終的交集就是空集。
  2. 對各個集合按照元素個數由少到多進行排序。這個排序有利於後面計算的時候從最小的集合開始,須要處理的元素個數較少。
  3. 對排序後第一個集合(也就是最小集合)進行遍歷,對於它的每個元素,依次在後面的全部集合中進行查找。只有在全部集合中都能找到的元素,才加入到最後的結果集合中。

須要注意的是,上述第 3 步在集合中進行查找,對於 intset 和 dict 的存儲來講時間複雜度分別是 O(log n) 和 O(1)。但因爲只有小集合才使用 intset,因此能夠粗略地認爲 intset 的查找也是常數時間複雜度的。

4.2 並集

並集操做最簡單,只要遍歷全部集合,將每個元素都添加到最後的結果集中便可。向集合中添加元素會自動去重,因此插入的時候無需檢測元素是否已存在。

4.3 差集

計算差集有兩種可能的算法,它們的時間複雜度有所區別。

第一種算法

對第一個集合進行遍歷,對於它的每個元素,依次在後面的全部集合中進行查找。只有在全部集合中都找不到的元素,才加入到最後的結果集合中。

這種算法的時間複雜度爲O(N*M),其中N是第一個集合的元素個數,M是集合數目。

第二種算法

  1. 將第一個集合的全部元素都加入到一箇中間集合中。
  2. 遍歷後面全部的集合,對於碰到的每個元素,從中間集合中刪掉它。
  3. 最後中間集合剩下的元素就構成了差集。
  4. 這種算法的時間複雜度爲O(N),其中N是全部集合的元素個數總和。

在計算差集的開始部分,會先分別估算一下兩種算法預期的時間複雜度,而後選擇複雜度低的算法來進行運算。還有兩點須要注意:

  • 在必定程度上優先選擇第一種算法,由於它涉及到的操做比較少,只用添加,而第二種算法要先添加再刪除。
  • 若是選擇了第一種算法,那麼在執行該算法以前,Redis的實現中對於第二個集合以後的全部集合,按照元素個數由多到少進行了排序。這個排序有利於以更大的機率查找到元素,從而更快地結束查找。

5 總結

  1. 整數集合是集合鍵的底層實現之一。
  2. 整數集合以有序、無重複的方式保存集合元素。在有須要時,會根據新添加元素的類型,改變底層數組的類型。
  3. 升級操做提高了操做的靈活性,並儘量的節約了內存。
  4. 集合能夠進行交、並、差集操做。
相關文章
相關標籤/搜索