1. 程式人生 > >redis原始碼分析與思考(十六)——集合型別的命令實現(t_set.c)

redis原始碼分析與思考(十六)——集合型別的命令實現(t_set.c)

    集合型別是用來儲存多個字串的,與列表型別不一樣,集合中不允許有重複的元素,也不能以索引的方式來通過下標獲取值,集合中的元素還是無序的。在普通的集合上增刪查改外,集合型別還實現了多個集合的取交集、並集、差集,集合的命令如下表所示:

集合命令表

命令 對應操作 時間複雜度
sadd key element [element…] 新增元素 O(n)
srem key element [element…] 刪除元素 O(n)
scard key 計算元素個數 O(1)
sismember key element 判斷元素是否在其中 O(1)
srandmember key [cout] 隨機從集合返回指定個數元素 O(n)
spop key 從集合中隨機彈出元素 O(n)
smembers key 獲取所有的元素 O(n)
sinter key [key…] 交集 O(n)
suinon key [key…] 並集 O(n)
sdiff key [key…] 差集 O(n)

    關於集合型別的命令實現,在這裡只討論結合型別的交、並、差集如何實現的以及它對應的轉碼操作,其餘的操作底層的原理是與列表、雜湊等型別實現是一致的。

編碼轉換

    當集合中的元素全為整數,且集合中的元素的數目不大於預設配置512個時,集合編碼採用整數集合實現,否則就進行編碼轉換,用雜湊表來實現集合。

int setTypeAdd(robj *subject, robj *value) {

   ......
   
    // intset
    } else if (subject->encoding == REDIS_ENCODING_INTSET) {
    //判斷其資料型別
        // 如果物件的值可以編碼為整數的話,那麼將物件的值新增到 intset 中
        if (isObjectRepresentableAsLongLong(value,&llval) == REDIS_OK) {
            uint8_t success = 0;
            subject->ptr = intsetAdd(subject->ptr,llval,&success);
            if (success) {
                /* Convert to regular set when the intset contains
                 * too many entries. */
                // 新增成功
                // 檢查集合在新增新元素之後是否需要轉換為字典,判斷其其長度
                if (intsetLen(subject->ptr) > server.set_max_intset_entries)
                    setTypeConvert(subject,REDIS_ENCODING_HT);
                return 1;
            }

      .......
  }   

//編碼轉換函式
/* 
 1. 將集合物件 setobj 的編碼轉換為 REDIS_ENCODING_HT 。
 2. 新建立的結果字典會被預先分配為和原來的集合一樣大。
 */
void setTypeConvert(robj *setobj, int enc) {
    setTypeIterator *si;
    // 確認型別和編碼正確
    redisAssertWithInfo(NULL,setobj,setobj->type == REDIS_SET &&
                             setobj->encoding == REDIS_ENCODING_INTSET);
    if (enc == REDIS_ENCODING_HT) {
        int64_t intele;
        // 建立新字典
        dict *d = dictCreate(&setDictType,NULL);
        robj *element;
        /* Presize the dict to avoid rehashing */
        // 預先擴充套件空間
        dictExpand(d,intsetLen(setobj->ptr));
        /* To add the elements we extract integers and create redis objects */
        // 遍歷集合,並將元素新增到字典中
        si = setTypeInitIterator(setobj);
        while (setTypeNext(si,NULL,&intele) != -1) {
            element = createStringObjectFromLongLong(intele);
           redisAssertWithInfo(NULL,element,dictAdd(d,element,NULL) == DICT_OK);
        }
        setTypeReleaseIterator(si);
        // 更新集合的編碼
        setobj->encoding = REDIS_ENCODING_HT;
        zfree(setobj->ptr);
        // 更新集合的值物件
        setobj->ptr = d;
    } else {
        redisPanic("Unsupported set conversion");
    }
}

交集

    取交集的策略是先對需要進行運算的集合進行按集合元素多少升序,然後逐步的用最少元素的集合與其它集合來做元素值的判斷,將共同擁有的元素返回給客戶端。如果有目標列表鍵存在,將共同擁有的值儲存在該鍵中:

/**
 3. 
 4. @param c 客戶端
 5. @param setkeys  需要取交集的集合
 6. @param setnum   集合的數目
 7. @param dstkey  儲存的列表鍵
 */
void sinterGenericCommand(redisClient *c, robj **setkeys, unsigned long setnum, robj *dstkey) {
    // 集合陣列
    robj **sets = zmalloc(sizeof(robj*)*setnum);
    setTypeIterator *si;
    robj *eleobj, *dstset = NULL;
    int64_t intobj;
    void *replylen = NULL;
    unsigned long j, cardinality = 0;
    int encoding;
    for (j = 0; j < setnum; j++) {
        // 取出物件
        // 第一次執行時,取出的是 dest 集合
        // 之後執行時,取出的都是 source 集合
        robj *setobj = dstkey ?
            lookupKeyWrite(c->db,setkeys[j]) :
            lookupKeyRead(c->db,setkeys[j]);
        // 物件不存在,放棄執行,進行清理
        if (!setobj) {
            zfree(sets);
            if (dstkey) {
                if (dbDelete(c->db,dstkey)) {
                    signalModifiedKey(c->db,dstkey);
                    server.dirty++;
                }
                addReply(c,shared.czero);
            } else {
                addReply(c,shared.emptymultibulk);
            }
            return;
        }
        // 檢查物件的型別
        if (checkType(c,setobj,REDIS_SET)) {
            zfree(sets);
            return;
        }
        // 將陣列指標指向集合物件
        sets[j] = setobj;
    }
    // 按基數對集合進行排序,這樣提升演算法的效率
    qsort(sets,setnum,sizeof(robj*),qsortCompareSetsByCardinality);
    // 因為不知道結果集會有多少個元素,所有沒有辦法直接設定回覆的數量
    // 這裡使用了一個小技巧,直接使用一個 BUFF 列表,
    // 然後將之後的回覆都新增到列表中
    if (!dstkey) {
        replylen = addDeferredMultiBulkLength(c);
    } else {
        /* If we have a target key where to store the resulting set
         * create this key with an empty set inside */
        dstset = createIntsetObject();
    }
    // 遍歷基數最小的第一個集合
    // 並將它的元素和所有其他集合進行對比
    // 如果有至少一個集合不包含這個元素,那麼這個元素不屬於交集
    si = setTypeInitIterator(sets[0]);
    while((encoding = setTypeNext(si,&eleobj,&intobj)) != -1) {
        // 遍歷其他集合,檢查元素是否在這些集合中存在
        for (j = 1; j < setnum; j++) {
            // 跳過第一個集合,因為它是結果集的起始值
            if (sets[j] == sets[0]) continue;
            // 元素的編碼為 INTSET 
            // 在其他集合中查詢這個物件是否存在
            if (encoding == REDIS_ENCODING_INTSET) {
                /* intset with intset is simple... and fast */
                if (sets[j]->encoding == REDIS_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 == REDIS_ENCODING_HT) {
                    eleobj = createStringObjectFromLongLong(intobj);
                    if (!setTypeIsMember(sets[j],eleobj)) {
                        decrRefCount(eleobj);
                        break;
                    }
                    decrRefCount(eleobj);
                }
            // 元素的編碼為 字典
            // 在其他集合中查詢這個物件是否存在
            } else if (encoding == REDIS_ENCODING_HT) {
                if (eleobj->encoding == REDIS_ENCODING_INT &&
                    sets[j]->encoding == REDIS_ENCODING_INTSET &&
                    !intsetFind((intset*)sets[j]->ptr,(long)eleobj->ptr))
                {
                    break;
                } else if (!setTypeIsMember(sets[j],eleobj)) {
                    break;
                }
            }
        }
        /* Only take action when all sets contain the member */
        // 如果所有集合都帶有目標元素的話,那麼執行以下程式碼
        if (j == setnum) {
            // SINTER 命令,直接返回結果集元素
            if (!dstkey) {
                if (encoding == REDIS_ENCODING_HT)
                    addReplyBulk(c,eleobj);
                else
                    addReplyBulkLongLong(c,intobj);
                cardinality++;
            // SINTERSTORE 命令,將結果新增到結果集中
            } else {
                if (encoding == REDIS_ENCODING_INTSET) {
                    eleobj = createStringObjectFromLongLong(intobj);
                    setTypeAdd(dstset,eleobj);
                    decrRefCount(eleobj);
                } else {
                    setTypeAdd(dstset,eleobj);
                }
            }
        }
    }
    setTypeReleaseIterator(si);
    // SINTERSTORE 命令,將結果集關聯到資料庫
    if (dstkey) {
        /* Store the resulting set into the target, if the intersection
         * is not an empty set. */
        // 刪除現在可能有的 dstkey
        int deleted = dbDelete(c->db,dstkey);
        // 如果結果集非空,那麼將它關聯到資料庫中
        if (setTypeSize(dstset) > 0) {
            dbAdd(c->db,dstkey,dstset);
            addReplyLongLong(c,setTypeSize(dstset));
            notifyKeyspaceEvent(REDIS_NOTIFY_SET,"sinterstore",
                dstkey,c->db->id);
        } else {
            decrRefCount(dstset);
            addReply(c,shared.czero);
            if (deleted)
                notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",
                    dstkey,c->db->id);
        }
        signalModifiedKey(c->db,dstkey);
        server.dirty++;
    // SINTER 命令,回覆結果集的基數
    } else {
        setDeferredMultiBulkLength(c,replylen,cardinality);
    }
    zfree(sets);
}

並集、差集

    並集與差集都是一個底層函式實現的,當程式識別是並集操作時,流程與取交集一樣,只不過是把沒有元素的加進去,而當程式識別是差集時,程式會根據不同的情況來選擇兩種間適合該情況的演算法來實現差集,兩種演算法簡要介紹如下:

1.程式遍歷 sets[0] 集合中的所有元素,並將這個元素和其他集合的所有元素進行對比,只有這個元素不存在於其他所有集合時,才將這個元素新增到結果集。這個演算法執行最多 N*M 步, N 是第一個集合的基數,而 M 是其他集合的數量。

2.將 sets[0] 的所有元素都新增到結果集中, 然後遍歷其他所有集合,將相同的元素從結果集中刪除,演算法複雜度為 O(N) ,N 為所有集合的基數之和。

//聯合
#define REDIS_OP_UNION 0
//差集
#define REDIS_OP_DIFF 1
//有序集合
#define REDIS_OP_INTER 2

/**
 * 
 * @param c 客戶端
 * @param setkeys 目標集合
 * @param setnum  集合的數目
 * @param dstkey  儲存的結果集合鍵
 * @param op 表明是什麼操作
 */
void sunionDiffGenericCommand(redisClient *c, robj **setkeys, int setnum, robj *dstkey, int op) {
    // 集合陣列
    robj **sets = zmalloc(sizeof(robj*)*setnum);
    setTypeIterator *si;
    robj *ele, *dstset = NULL;
    int j, cardinality = 0;
    int diff_algo = 1;
    // 取出所有集合物件,並新增到集合陣列中
    for (j = 0; j < setnum; j++) {
        robj *setobj = dstkey ?
            lookupKeyWrite(c->db,setkeys[j]) :
            lookupKeyRead(c->db,setkeys[j]);
        // 不存在的集合當作 NULL 來處理
        if (!setobj) {
            sets[j] = NULL;
            continue;
        }
        // 有物件不是集合,停止執行,進行清理
        if (checkType(c,setobj,REDIS_SET)) {
            zfree(sets);
            return;
        }
        // 記錄物件
        sets[j] = setobj;
    }
    /*
     * 選擇使用那個演算法來執行計算
     * 演算法 1 的複雜度為 O(N*M) ,其中 N 為第一個集合的基數,
     * 而 M 則為其他集合的數量。
     * 演算法 2 的複雜度為 O(N) ,其中 N 為所有集合中的元素數量總數。
     * 程式通過考察輸入來決定使用那個演算法
     */
    if (op == REDIS_OP_DIFF && sets[0]) {
        long long algo_one_work = 0, algo_two_work = 0;
        // 遍歷所有集合
        for (j = 0; j < setnum; j++) {
            if (sets[j] == NULL) continue;
            // 計算 setnum 乘以 sets[0] 的基數之積
            algo_one_work += setTypeSize(sets[0]);
            // 計算所有集合的基數之和
            algo_two_work += setTypeSize(sets[j]);
        }
        // 演算法 1 的常數比較低,優先考慮演算法 1
        algo_one_work /= 2;
        diff_algo = (algo_one_work <= algo_two_work) ? 1 : 2;

        if (diff_algo == 1 && setnum > 1) {
         //果使用的是演算法 1 ,那麼最好對 sets[0] 以外的其他集合進行排序
            // 這樣有助於優化演算法的效能
            qsort(sets+1,setnum-1,sizeof(robj*),
                qsortCompareSetsByRevCardinality);
        }
    }
    /* 
     * 使用一個臨時集合來儲存結果集,如果程式執行的是 SUNIONSTORE 命令,
     * 那麼這個結果將會成為將來的集合值物件。
     */
    dstset = createIntsetObject();
    // 執行的是並集計算
    if (op == REDIS_OP_UNION) {
        /* Union is trivial, just add every element of every set to the
         * temporary set. */
        // 遍歷所有集合,將元素新增到結果集裡就可以了
        for (j = 0; j < setnum; j++) {
            if (!sets[j]) continue; /* non existing keys are like empty sets */
            si = setTypeInitIterator(sets[j]);
            while((ele = setTypeNextObject(si)) != NULL) {
                // setTypeAdd 只在集合不存在時,才會將元素新增到集合,並返回 1 
                if (setTypeAdd(dstset,ele)) cardinality++;
                decrRefCount(ele);
            }
            setTypeReleaseIterator(si);
        }
    // 執行的是差集計算,並且使用演算法 1
    } else if (op == REDIS_OP_DIFF && sets[0] && diff_algo == 1) { 
         // 差集演算法 
        si = setTypeInitIterator(sets[0]);
        while((ele = setTypeNextObject(si)) != NULL) {
            // 檢查元素在其他集合是否存在
            for (j = 1; j < setnum; j++) {
                if (!sets[j]) continue; /* no key is an empty set. */
                if (sets[j] == sets[0]) break; /* same set! */
                if (setTypeIsMember(sets[j],ele)) break;
            }
            // 只有元素在所有其他集合中都不存在時,才將它新增到結果集中
            if (j == setnum) {
                /* There is no other set with this element. Add it. */
                setTypeAdd(dstset,ele);
                cardinality++;
            }
            decrRefCount(ele);
        }
        setTypeReleaseIterator(si);
    // 執行的是差集計算,並且使用演算法 2
    } else if (op == REDIS_OP_DIFF && sets[0] && diff_algo == 2) {
         //差集演算法 2 :
        for (j = 0; j < setnum; j++) {
            if (!sets[j]) continue; /* non existing keys are like empty sets */
            si = setTypeInitIterator(sets[j]);
            while((ele = setTypeNextObject(si)) != NULL) {
                // sets[0] 時,將所有元素新增到集合
                if (j == 0) {
                    if (setTypeAdd(dstset,ele)) cardinality++;
                // 不是 sets[0] 時,將所有集合從結果集中移除
                } else {
                    if (setTypeRemove(dstset,ele)) cardinality--;
                }
                decrRefCount(ele);
            }
            setTypeReleaseIterator(si);
            /* Exit if result set is empty as any additional removal
             * of elements will have no effect. */
            if (cardinality == 0) break;
        }
    }
    // 執行的是 SDIFF 或者 SUNION
    // 列印結果集中的所有元素
    if (!dstkey) {
        addReplyMultiBulkLen(c,cardinality);
        // 遍歷並回復結果集中的元素
        si = setTypeInitIterator(dstset);
        while((ele = setTypeNextObject(si)) != NULL) {
            addReplyBulk(c
            
           

相關推薦

redis原始碼分析思考——集合型別命令實現(t_set.c)

    集合型別是用來儲存多個字串的,與列表型別不一樣,集合中不允許有重複的元素,也不能以索引的方式來通過下標獲取值,集合中的元素還是無序的。在普通的集合上增刪查改外,集合型別還實現了多個集合的取交集、並集、差集,集合的命令如下表所示: 集合命

redis原始碼分析思考——列表型別命令實現(t_list.c)

    列表型別是用來存貯多個字串物件的結構。一個列表可以存貯232-1個元素,可以對列表兩端進行插入(push)、彈出(pop),還可以獲取指定範圍內的元素列表、獲取指定索引的元素等等,它可以靈活的充當棧和佇列的角色。下面列出列表的命令: 列

redis原始碼分析思考——AOF持久化

    為了解決持久化檔案很龐大以及會阻塞伺服器的 情況,redis提出一種新的持久化方案:AOF持久化。AOF持久化是redis儲存資料的另外一種方式,全稱Append Only File,與RDB持久化不同的是,AOF持久化是隻儲存從客戶端鍵入

redis原始碼分析思考——RDB持久化

    redis是一個鍵值對的資料庫伺服器,伺服器中包含著若干個非空的資料庫,每個非空資料庫裡又包含著若干個鍵值對。因為redis是一個基於記憶體存貯的資料庫,他將自己所存的資料存於記憶體中,如果不將這些資料及時的儲存在硬碟中,當電腦關機或者進行

redis原始碼分析思考——有序集合型別命令實現(t_zset.c)

    有序集合是集合的延伸,它儲存著集合元素的不可重複性,但不同的是,它是有序的,它利用每一個元素的分數來作為有序集合的排序依據,現在列出有序集合的命令: 有序集合命令 命令 對應操作 時

redis原始碼分析思考——雜湊型別命令實現(t_hash.c)

    雜湊型別又叫做字典,在redis中,雜湊型別本身是一個鍵值對,而雜湊型別裡面也存貯著鍵值對,其對應關係是,每個雜湊型別的值對應著一個鍵值對或多對鍵值對,如圖所示: 雜湊型別命令 命令 對應操

redis原始碼分析思考——有序集合型別命令實現(t_set.c)

    有序集合是集合的延伸,它儲存著集合元素的不可重複性,但不同的是,它是有序的,它利用每一個元素的分數來作為有序集合的排序依據,現在列出有序集合的命令: 有序集合命令 命令 對應操作 時間複

redis原始碼分析思考十三——字串型別命令實現(t_string.c)

    在對字串操作的命令中,主要有增加刪查該、批處理操作以及編碼的轉換命令,現在列出對字串物件操作的主要常用命令: 常用命令表 命令 對應操作 時間複雜度

redis原始碼分析思考——字典中鍵的兩種hash演算法

      在Redis字典中,得到鍵的hash值顯得尤為重要,因為這個不僅關乎到是否字典能做到負載均衡,以及在效能上優勢是否突出,一個良好的hash演算法在此時就能發揮出巨大的作用。而一個良好的has

redis原始碼分析思考——物件

    談及物件,我們不免會立即聯想到Java、C++等面向物件的語言,而在C中是沒有物件這一說法的,為了方便管理與程式碼整體的優化,redis基於前面幾篇部落格的資料結構自建了一套物件系統。這個系統包含著字串物件、列表物件、雜湊物件、集合物件以及有序集合物件。

redis原始碼分析思考——sds

  在閱讀黃健巨集的書《Redis設計與實現》的時候,深刻的意識到僅僅看別人的作品是遠遠不夠,自己更應該去閱讀原始碼,形成自己的思考,這樣才算真正的學進去了。   現如今,Nosql的概念大行其道,redis作為其中的佼佼者被廣大的開發者愛好著,而且Redis的原始碼僅僅只

element-ui Carousel 走馬燈原始碼分析整理筆記

Carousel 走馬燈原始碼分析整理筆記,這篇寫的不詳細,後面有空補充 main.vue <template> <!--走馬燈的最外層包裹div--> <div class="el-carousel" :class="{ 'el-carousel--card

Redis原始碼剖析和註釋---- Redis 資料庫及相關命令實現(db)

Redis 資料庫及相關命令實現 1. 資料庫管理命令 命令 描述 FLUSHDB 清空當前資料庫的所有key FLUSHALL 清空整個Redis伺服器的所有key DBSIZE 返回當前資料庫的

百度大腦人臉識別深度驗證思考之動態實時

前言 我已經厭倦了靜態圖片的識別,那些技術對我已經沒有了挑戰性。今天我們就來看看動態實時的深度識別表現如何。 攝像頭 央視 我們直接採集央視rtmp推流地址的視訊,直接進行人臉識別和即時

機器學習理論實戰概率圖模型04

        04、概率圖模型應用例項         最近一篇文章《Deformable Model Fitting by Regularized Landmark Mean-Shift》中的人臉點檢測演算法在速度和精度折中上達到了一個相對不錯的水平,這篇技術報告就來闡

百度大腦人臉識別深度驗證思考之斷章

點開這篇博文,讓我帶你進入一個全新的世界,那是一片我們所有人從未涉足過的領域,充滿了玄妙、驚愕和震撼,感謝百度大腦這個人工智慧,讓我們可以有機會推開那扇小小的神奇之門。 前言 乍一看標題,覺得有些愕然,但這篇博文不以標題見長。用到斷章一詞,是因為其極為符合這

關於大型網站技術演進的思考--網站靜態化處理—前後端分離—下8

  我第一次聽說nodejs技術大概是在2009年年末,不過我真正認真在網路上進一步瞭解nodejs還是在2010年年中,當時對nodejs的認識和我現在對nodejs的認識有著天壤的區別,開始想了解nodejs我只是為了感慨谷歌公司開發的V8引擎居然如此強大,它不僅僅可以作為chrome瀏覽器的javasc

Redis原始碼剖析和註釋--- Redis 事件處理實現

Redis 事件處理實現 1. Redis事件介紹 Redis伺服器是一個事件驅動程式。下面先來簡單介紹什麼是事件驅動。 所謂事件驅動,就是當你輸入一條命令並且按下回車,然後訊息被組裝成Redis協議的格式傳送給Redis伺服器,這就會產生一個事件,Red

Redis原始碼剖析和註釋--- 雜湊鍵命令實現(t_hash)

Redis 雜湊鍵命令實現(t_hash) 1. 雜湊命令介紹 Redis 所有雜湊命令如下表所示:Redis 雜湊命令詳解 序號 命令及描述 1 HDEL key field2 [field2]:刪除一個或多個雜湊表字段

Android項目實戰:QQ空間實現—— 展示說說中的評論內容並有相應點擊事件

con toast short demo append 集合 obj parent 自帶 原文:Android項目實戰(十六):QQ空間實現(一)—— 展示說說中的評論內容並有相應點擊事件大家都玩QQ空間客戶端,對於每一個說說,我們都可以評論,那麽,對於某一條評論: