Redis源碼剖析 集合對象t_set實現

從之前的章節“Redis源碼剖析–對象Object” 可以知道,redis中的SET(集合)有兩種可能的數據存儲方式。分別是整數集合REDIS_ENCODING_INTSET和哈希表REDIS_ENCODING_HT。

robj *setTypeCreate(sds value) {
    if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
        return createIntsetObject();
    return createSetObject();
}

第一個添加到集合的元素決定了初始的存儲方式,如果第一個原始是一個整數,初始的編碼就會是REDIS_ENCODING_INTSET,否則就是REDIS_ENCODING_HT,所以集合中的數據操作的時候基本都涉及到兩個存儲方式的判斷。

SET的結構

typedef struct redisObject {
    unsigned type:4;  // 此字段爲OBJ_SET
    unsigned encoding:4;  // 如果是set結構,編碼爲OBJ_ENCODING_INTSET或OBJ_ENCODING_HT
    unsigned lru:LRU_BITS; 
    int refcount;
    void *ptr;
} robj;

同其他的對象一樣, set結構也是存儲在redisObject結構體中,通過指定 type=OBJ_SET 來確定這是一個集合對象,當是一個集合對象的時候,配套的endoding只能有對應的兩種取值。

SET命令

命令 說明
sadd 將一個或多個 member 元素加入到集合 key 當中,已經存在於集合的 member 元素將被忽略
scard 返回集合 key 的基數(集合中元素的數量)
spop 移除並返回集合中的一個隨機元素
smembers 返回集合 key 中的所有成員
sismember 判斷 member 元素是否集合 key 的成員
sinter 返回多個集合的交集,多個集合由 keys 指定
srandmember 返回集合中的一個隨機元素
srandmember 返回集合中的 count 個隨機元素
srem 移除集合 key 中的一個或多個 member 元素,不存在的 member 元素會被忽略
sunion 返回多個集合的並集,多個集合由 keys 指定
sdiff 返回一個集合的全部成員,該集合是所有給定集合之間的差集

SET命令實現

SET編碼轉換

如果一個集合使用 REDIS_ENCODING_INTSET 編碼, 那麼當以下任何一個條件被滿足時, 這個集合會被轉換成 REDIS_ENCODING_HT 編碼:

  • intset 保存的整數值個數超過
  • server.set_max_intset_entries (默認值爲 512 )。
    試圖往集合裏添加一個新元素,並且這個元素不能被表示爲 long long 類型(也即是,它不是一個整數)。
/* Convert the set to specified encoding. The resulting dict (when converting
 * to a hash table) is presized to hold the number of elements in the original
 * set. */
void setTypeConvert(robj *setobj, int enc) {
    setTypeIterator *si;
    serverAssertWithInfo(NULL,setobj,setobj->type == OBJ_SET &&
                             setobj->encoding == OBJ_ENCODING_INTSET);

    if (enc == OBJ_ENCODING_HT) {
        int64_t intele;
        dict *d = dictCreate(&setDictType,NULL);
        sds element;

        /*  提前擴張字典結構的大小,避免出現rehash過程 */
        dictExpand(d,intsetLen(setobj->ptr));

        /* To add the elements we extract integers and create redis objects */
        si = setTypeInitIterator(setobj); // 初始化迭代器
        // 循環遍歷intset,以及加入到dict中
        while (setTypeNext(si,&element,&intele) != -1) {
            element = sdsfromlonglong(intele);
            serverAssert(dictAdd(d,element,NULL) == DICT_OK);
        }
        setTypeReleaseIterator(si); // 釋放迭代器

        // 設定轉換後的編碼類型
        setobj->encoding = OBJ_ENCODING_HT;
        zfree(setobj->ptr);
        setobj->ptr = d; // 指針指向新創建的dict
    } else {
        // 如果要轉換的類型不是OBJ_ENCODING_HT,退出
        serverPanic("Unsupported set conversion");
    }
}

當SET用HT編碼的時候,所有元素其實是保存在字典的鍵裏邊,而字典的值全都設置成了NULL,這樣就能保證集合中所有元素的唯一性。

SADD接口實現

/* Add the specified value into a set.
 *
 * If the value was already member of the set, nothing is done and 0 is
 * returned, otherwise the new element is added and 1 is returned. */
// 插入元素到集合,如果元素已存在,返回0, 插入成功返回1
int setTypeAdd(robj *subject, sds value) {
    long long llval;
    // 如果是HT編碼結構,在dict中填加元素
    if (subject->encoding == OBJ_ENCODING_HT) {
        dict *ht = subject->ptr;
        dictEntry *de = dictAddRaw(ht,value,NULL);
        if (de) {
            dictSetKey(ht,de,sdsdup(value));
            dictSetVal(ht,de,NULL);
            return 1;
        }
    } else if (subject->encoding == OBJ_ENCODING_INTSET) {
        // 如果是整數集合編碼結構,判斷新加的元素是否能夠用long long表示
        if (isSdsRepresentableAsLongLong(value,&llval) == C_OK) {
            // 如果是整數結構,在intset中插入整數
            uint8_t success = 0;
            subject->ptr = intsetAdd(subject->ptr,llval,&success);
            if (success) {
                /* Convert to regular set when the intset contains
                 * too many entries. */
                // 插入成功之後,判斷是否超出範圍,如果超過了set_max_intset_entries,需要做結構轉換
                if (intsetLen(subject->ptr) > server.set_max_intset_entries)
                    setTypeConvert(subject,OBJ_ENCODING_HT);
                return 1;
            }
        } else {
            // 如果要插入的不是整數,先將原先的intset做結構轉換,然後執行插入
            /* Failed to get integer from object, convert to regular set. */
            setTypeConvert(subject,OBJ_ENCODING_HT);

            /* The set *was* an intset and this value is not integer
             * encodable, so dictAdd should always work. */
            serverAssert(dictAdd(subject->ptr,sdsdup(value),NULL) == DICT_OK);
            return 1;
        }
    } else {
        serverPanic("Unknown set encoding");
    }
    return 0;
}

往SET中插入的時候,需要先判斷當前的SET的底層結構,如果是HT格式,直接調用dict的插入命令;如果是intset結構,需要先判斷要插入的新元素是否也是整數,如果不是整數,需要將SET的編碼格式轉換爲HT結構體,再執行插入,如果是整數,正常插入之後也要檢查一遍是否超過了intset的最大設置。

其他的比如移除元素之類的操作,也是類似,需要考慮在不同的編碼格式下的處理方案。

集合命令的實現

SET相比較其他格式的結構,多了集合命令的實現,就是我們所熟知的集合的交差並集。這邊講一下求交集的算法。

求交集算法

/* Iterate all the elements of the first (smallest) set, and test
     * the element against all the other sets, if at least one set does
     * not include the element it is discarded */
    // 獲取第一個(基數最小的)集合,循環遍歷
    si = setTypeInitIterator(sets[0]);
    while((encoding = setTypeNext(si,&elesds,&intobj)) != -1) {
        for (j = 1; j < setnum; j++) {
            if (sets[j] == sets[0]) continue;
            // 如果底層編碼是INTSET
            if (encoding == OBJ_ENCODING_INTSET) {
                /* intset with intset is simple... and fast */
                if (sets[j]->encoding == OBJ_ENCODING_INTSET &&
                    !intsetFind((intset*)sets[j]->ptr,intobj))
                {
                    break;
                /* in order to compare an integer with an object we
                 * have to use the generic function, creating an object
                 * for this */
                } else if (sets[j]->encoding == OBJ_ENCODING_HT) {
                    elesds = sdsfromlonglong(intobj);
                    if (!setTypeIsMember(sets[j],elesds)) {
                        sdsfree(elesds);
                        break;
                    }
                    sdsfree(elesds);
                }
            } else if (encoding == OBJ_ENCODING_HT) {
                // 如果底層編碼HT
                if (!setTypeIsMember(sets[j],elesds)) {
                    break;
                }
            }
        }

        /* Only take action when all sets contain the member */
        // 如果j==setNum,說明已經檢查到了最後,所有的set都有這個元素
        if (j == setnum) {
            if (!dstkey) {
                if (encoding == OBJ_ENCODING_HT)
                    addReplyBulkCBuffer(c,elesds,sdslen(elesds));
                else
                    addReplyBulkLongLong(c,intobj);
                cardinality++;
            } else {
                if (encoding == OBJ_ENCODING_INTSET) {
                    elesds = sdsfromlonglong(intobj);
                    setTypeAdd(dstset,elesds);
                    sdsfree(elesds);
                } else {
                    setTypeAdd(dstset,elesds);
                }
            }
        }
    }
    setTypeReleaseIterator(si);

    // 如果目標key存在,保存結果
    if (dstkey) {
        /* Store the resulting set into the target, if the intersection
         * is not an empty set. */
        int deleted = dbDelete(c->db,dstkey);
        // 如果最終的結果set非空, 將最終set結果放到dstkey中
        if (setTypeSize(dstset) > 0) {
            dbAdd(c->db,dstkey,dstset);
            addReplyLongLong(c,setTypeSize(dstset));
            notifyKeyspaceEvent(NOTIFY_SET,"sinterstore",
                dstkey,c->db->id);
        } else {
            // 如果結果集合爲空,刪除
            decrRefCount(dstset);
            addReply(c,shared.czero);
            if (deleted)
                notifyKeyspaceEvent(NOTIFY_GENERIC,"del",
                    dstkey,c->db->id);
        }
        signalModifiedKey(c->db,dstkey);
        server.dirty++;
    } else {
        setDeferredSetLen(c,replylen,cardinality);
    }
    zfree(sets);
}

通俗的來講,求交集主要分爲以下幾個步驟:

  1. 將所有的集合按照基數進行排序
  2. 使用基數最小的集合作爲結果集
  3. 遍歷這個基數最小集合中的每個元素,檢查這個元素在剩餘的其他集合中是否存在
  4. 如果都存在,就將這個元素加入到最終的結果集中

算法的複雜度是O(N2)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章