《redis设计与实现》-8集合对象

一 序

     之前的时候,拆二代(日天哥)问我集合的sdiff命令是怎么实现的?我说不知道,需要看代码才能知道。所以有了这篇。书上照例是举例说明编码及转换,还是按照惯例,先看编码、再看命令实现。谨以此文送给分享过disruptor的二代。

二 t_set

   之前在《redis设计与实现》-第8章object介绍过:一个集合类型的对象的编码有两种,分别是OBJ_ENCODING_HT和OBJ_ENCODING_INTSET。

编码—encoding对象—ptr
OBJ_ENCODING_HT字典实现的集合对象
OBJ_ENCODING_INTSET整数集合实现的集合对象

在object.c,可以看到setObject的创建跟释放过程是跟上面大的编码有关联的。

robj *createSetObject(void) {
    dict *d = dictCreate(&setDictType,NULL);
    robj *o = createObject(OBJ_SET,d);
    o->encoding = OBJ_ENCODING_HT;
    return o;
}
void freeSetObject(robj *o) {
    switch (o->encoding) {
    case OBJ_ENCODING_HT:
        dictRelease((dict*) o->ptr);
        break;
    case OBJ_ENCODING_INTSET:
        zfree(o->ptr);
        break;
    default:
        serverPanic("Unknown set encoding type");
    }
}

关于集合类型底层的两种数据结构的源码剖析和注释,请看下面的博文。

《redis设计与实现》-4 字典

《redis设计与实现》-第6章整数集合intset

从OBJ_ENCODING_INTSET转换到OBJ_ENCODING_HT的条件如下:

1)如果数据编码为整数集合的集合对象的元素数量超过 set-max-intset-entries 阈值(默认512),则会转换编码。

2) 集合保存对象所有的元素都是整数值。

看下具体的代码实现,在t-set.c


/* Add the specified value into a set. The function takes care of incrementing
 * the reference count of the object if needed in order to retain a copy.
 *
 * 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. */
int setTypeAdd(robj *subject, robj *value) {
    long long llval;
    if (subject->encoding == OBJ_ENCODING_HT) { // 字典
    	   // 将 value 作为键, NULL 作为值,将元素添加到字典中
        if (dictAdd(subject->ptr,value,NULL) == DICT_OK) {
            incrRefCount(value);
            return 1;
        }
    } else if (subject->encoding == OBJ_ENCODING_INTSET) { //intset
    	   // 如果对象的值可以编码为整数的话,那么将对象的值添加到 intset 中
        if (isObjectRepresentableAsLongLong(value,&llval) == C_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,OBJ_ENCODING_HT);//转换
                return 1;
            }
        } else {//走到这里就是不能编码为整数,那么将集合从 intset 走到这里就是编码转换为 HT 编码
            /* 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.
             * 执行添加操作
              */
            serverAssertWithInfo(NULL,value,
                                dictAdd(subject->ptr,value,NULL) == DICT_OK);
            incrRefCount(value);
            return 1;
        }
    } else {  // 未知编码错误
        serverPanic("Unknown set encoding");
    }
    return 0;
}

上面的两点限制:编码跟长度都在此展示。好了,看下具体的集合对象的数据编码转换过程setTypeConvert。t_set.c


/* 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. */
 // 将集合对象的INTSET编码类型转换为enc类型
 //新创建的结果字典会被预先分配为和原来的集合一样大。
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) { // 转换成OBJ_ENCODING_HT字典类型的编码,只能单向转。
        int64_t intele;
        dict *d = dictCreate(&setDictType,NULL); // 创建一个字典
        robj *element;

        /* Presize the dict to avoid rehashing */
        dictExpand(d,intsetLen(setobj->ptr));// 预先扩展空间,避免rehash.
        
        // 遍历集合,并将元素添加到字典中
        /* To add the elements we extract integers and create redis objects 创建并初始化一个集合类型的迭代器*/
        si = setTypeInitIterator(setobj);
        // 迭代器整数集合
        while (setTypeNext(si,&element,&intele) != -1) {
            element = createStringObjectFromLongLong(intele); //将当前集合中的元素转换为字符串类型对象
            serverAssertWithInfo(NULL,element,
                                dictAdd(d,element,NULL) == DICT_OK);//添加到字典
        }
        setTypeReleaseIterator(si);  // 释放迭代器空间

        setobj->encoding = OBJ_ENCODING_HT;// 更新集合的编码
        zfree(setobj->ptr);
        setobj->ptr = d; // 更新集合的值对象
    } else {
        serverPanic("Unsupported set conversion");
    }
}

书上分别举例,

SADD numbers 1 3 5

SADD fruits "apple" "banana" "cherry"

集合类型封装的API,在server.h

/* Set data type */
robj *setTypeCreate(robj *value);// 创建一个保存value的集合
int setTypeAdd(robj *subject, robj *value);// 向subject集合中添加value,添加成功返回1,如果已经存在返回0
int setTypeRemove(robj *subject, robj *value);// 从集合对象中删除一个值为value的元素,删除成功返回1,失败返回0
int setTypeIsMember(robj *subject, robj *value);// 集合中是否存在值为value的元素,存在返回1,否则返回0
setTypeIterator *setTypeInitIterator(robj *subject);// 创建并初始化一个集合类型的迭代器
void setTypeReleaseIterator(setTypeIterator *si);// 释放迭代器空间
int setTypeNext(setTypeIterator *si, robj **objele, int64_t *llele);//迭代器吧结果保存到参数中
robj *setTypeNextObject(setTypeIterator *si);// 返回迭代器当前指向的元素对象的地址,需要手动释放返回的对象
int setTypeRandomElement(robj *setobj, robj **objele, int64_t *llele);// 从集合中随机取出一个对象,保存在参数中
unsigned long setTypeRandomElements(robj *set, unsigned long count, robj *aux_set);
unsigned long setTypeSize(robj *subject);// 返回集合的元素数量
void setTypeConvert(robj *subject, int enc);// 将集合对象的INTSET编码类型转换为enc类型

集合类型实现了自己的迭代器,也基于字典的迭代器封装的.

/* Structure to hold set iteration abstraction. */
typedef struct {
    robj *subject;//所属的集合对象
    int encoding;   //集合对象编码类型
    int ii; /* intset iterator *///整数集合的迭代器,编码为INTSET使用
    dictIterator *di;  //字典的迭代器,编码为HT使用
} setTypeIterator;
  • 创建并初始化一个集合类型的迭代器

setTypeIterator *setTypeInitIterator(robj *subject) {
	  // 分配空间并初始化成员
    setTypeIterator *si = zmalloc(sizeof(setTypeIterator));
    si->subject = subject;
    si->encoding = subject->encoding;
    if (si->encoding == OBJ_ENCODING_HT) {// 初始化字典的迭代器
        si->di = dictGetIterator(subject->ptr);
    } else if (si->encoding == OBJ_ENCODING_INTSET) { // 初始化集合的迭代器,该成员为集合的下标
        si->ii = 0;
    } else {
        serverPanic("Unknown set encoding");
    }
    return si;
}

void setTypeReleaseIterator(setTypeIterator *si) {
    if (si->encoding == OBJ_ENCODING_HT)// 如果是字典类型,需要先释放字典类型的迭代器
        dictReleaseIterator(si->di);
    zfree(si);//释放迭代器
}

迭代操作:

/* Move to the next entry in the set. Returns the object at the current
 * position.
 *
 * Since set elements can be internally be stored as redis objects or
 * simple arrays of integers, setTypeNext returns the encoding of the
 * set object you are iterating, and will populate the appropriate pointer
 * (objele) or (llele) accordingly.
 ** 因为集合即可以编码为 intset ,也可以编码为哈希表,
 * 所以程序会根据集合的编码,选择将值保存到那个参数里:
 *
 *  - 当编码为 intset 时,元素被指向到 llobj 参数
 *
 *  - 当编码为哈希表时,元素被指向到 eobj 参数
 *
 * 并且函数会返回被迭代集合的编码,方便识别。
 * Note that both the objele and llele pointers should be passed and cannot
 * be NULL since the function will try to defensively populate the non
 * used field with values which are easy to trap if misused.
 *
 * When there are no longer elements -1 is returned.
 * Returned objects ref count is not incremented, so this function is
 * copy on write friendly. */
int setTypeNext(setTypeIterator *si, robj **objele, int64_t *llele) {
    if (si->encoding == OBJ_ENCODING_HT) {// 迭代字典,取出对象
        dictEntry *de = dictNext(si->di);   // 更新迭代器
        if (de == NULL) return -1; // 字典已迭代完
        *objele = dictGetKey(de);  // 返回节点的键(集合的元素)
        *llele = -123456789; /* Not needed. Defensive. */
    } else if (si->encoding == OBJ_ENCODING_INTSET) { // 迭代intset,取出元素
        if (!intsetGet(si->subject->ptr,si->ii++,llele)) // 从intset中保存元素到llele中
            return -1;
        *objele = NULL; /* Not needed. Defensive. */
    } else {
        serverPanic("Wrong set encoding in setTypeNext");
    }
    return si->encoding; //返回编码类型
}

/* The not copy on write friendly version but easy to use version
 * of setTypeNext() is setTypeNextObject(), returning new objects
 * or incrementing the ref count of returned objects. So if you don't
 * retain a pointer to this object you should call decrRefCount() against it.
 * setTypeNext 的非 copy-on-write 友好版本,
 * 总是返回一个新的、或者已经增加过引用计数的对象。
 * 调用者在使用完对象之后,应该对对象调用 decrRefCount() 。
 * This function is the way to go for write operations where COW is not
 * an issue as the result will be anyway of incrementing the ref count.
 * 这个函数应该在非 copy-on-write 时调用。
  */
robj *setTypeNextObject(setTypeIterator *si) {
    int64_t intele;
    robj *objele;
    int encoding;
    // 得到当前集合对象的编码类型
    encoding = setTypeNext(si,&objele,&intele);
    switch(encoding) {
        case -1:    return NULL;//迭代完成
        case OBJ_ENCODING_INTSET://整数集合 返回一个整数值,需要为这个值创建String对象
            return createStringObjectFromLongLong(intele);
        case OBJ_ENCODING_HT: //字典集合, HT 本身已经返回对象了,只需执行 incrRefCount()
            incrRefCount(objele);
            return objele;
        default:
            serverPanic("Unsupported encoding");
    }
    return NULL; /* just to suppress warnings */
}

三 集合命令的实现

3.1 求交集sinter 

 sinter求交集, sinterstore求交集并保存结果,

void sinterCommand(client *c) {
    sinterGenericCommand(c,c->argv+1,c->argc-1,NULL);
}

void sinterstoreCommand(client *c) {
    sinterGenericCommand(c,c->argv+2,c->argc-2,c->argv[1]);
}

我自己觉得不太懂C语言看起来就是别扭,需要去猜测。比如这段代码,属于C的基本功了,不会的就需要去理解下加个函数的入参:sinterGenericCommand(client *c, robj **setkeys,  unsigned long setnum, robj *dstkey) 

SINTER key [key ...]

SINTERSTORE destination key [key ...]

第一个参数不用说了,从第二个setkeys说起,就事要比较的那些集合:sinter 是argv+1,表示从参数的第1个开始,同理argv+2。可以结合命令去看下,是不是所有的要处理的集合。agrc通常表示参数的长度,对应的setnum集合数量。sinter是argc -1.表示参数除了第一个外是sinter外,其余的是集合数量。同理sinterstore argc2,看redis的过程中免不了遇到类似问题,大神不会每一行都注释的,关键节点会有,太基础的还需啊哟自己去理解。切入正文看函数本身。

void sinterGenericCommand(client *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++) {
    	  //取出对象,下面是结合入参判断命令,因为SINTER,SINTERSTORE命令都调用了sinterGenericCommand,只是参数dstkey不同
    	  //dstkey非空,是SINTERSTORE命令:通过写操作读取集合对象,否则是SINTER命令则通过读操作读取集合对象
        robj *setobj = dstkey ?
            lookupKeyWrite(c->db,setkeys[j]) :
            lookupKeyRead(c->db,setkeys[j]);
        // 读取的集合对象不存在,执行清理操作
        if (!setobj) {
            zfree(sets); //释放集合数组空间
            if (dstkey) {// dstkey非空,从数据库中删除存储的目标集合对象dstkey
                if (dbDelete(c->db,dstkey)) {
                    signalModifiedKey(c->db,dstkey);//通知
                    server.dirty++;//设脏
                }
                addReply(c,shared.czero);//发送zero(0)给client
            } else {
                addReply(c,shared.emptymultibulk);//发送空给客户端
            }
            return;
        }
        if (checkType(c,setobj,OBJ_SET)) {//读取集合对象成功,则检查其数据类型
            zfree(sets);
            return;
        }
        sets[j] = setobj;//数组指针指向集合对象(就是将读取出的对象保存在集合数组中)
    }
    /* Sort sets from the smallest to largest, this will improve our
     * algorithm's performance 按基数对集合进行从小到达进行排序,这样提升算法的效率,看作者的注释很重要,尤其是你第一次看
     */
    qsort(sets,setnum,sizeof(robj*),qsortCompareSetsByCardinality);

    /* The first thing we should output is the total number of elements...
     * since this is a multi-bulk write, but at this stage we don't know
     * the intersection set size, so we use a trick, append an empty object
     * to the output list and save the pointer to later modify it with the
     * right length 
     * 因为不知道结果集会有多少个元素,所有没有办法直接设置回复的数量
     * 这里使用了一个小技巧,直接使用一个 空列表,用来保存所有的回复
     */
    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();//创建要给整数集合对象
    }

    /* 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,&eleobj,&intobj)) != -1) {
    	   // 遍历其他集合,检查元素是否在这些集合中存在
        for (j = 1; j < setnum; j++) {
        	// 跳过第一个集合,因为它们有相同的元素
            if (sets[j] == sets[0]) continue;
            if (encoding == OBJ_ENCODING_INTSET) { // 当前元素为INTSET类型(目的在其他集合中查找这个对象是否存在,下面会根据不同编码来区分)
                /* intset with intset is simple... and fast */
                // 如果在当前intset集合中没有找到该元素则直接跳过当前元素,操作下一个元素
                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) { // 在字典中查找要麻烦一些,
                    eleobj = createStringObjectFromLongLong(intobj);// 创建字符串对象
                    if (!setTypeIsMember(sets[j],eleobj)) { // 如果当前元素不是当前集合中的元素,则操作下一个元素
                        decrRefCount(eleobj);//释放字符串对象
                        break;
                    }
                    decrRefCount(eleobj);
                }
            } else if (encoding == OBJ_ENCODING_HT) {//元素的编码为字典类型
                /* Optimization... if the source object is integer
                 * encoded AND the target set is an intset, we can get
                 * a much faster path. */
                 // 当前元素的编码是int类型且当前集合为整数集合,如果该集合不包含该元素,则跳过循环
                if (eleobj->encoding == OBJ_ENCODING_INT &&
                    sets[j]->encoding == OBJ_ENCODING_INTSET &&
                    !intsetFind((intset*)sets[j]->ptr,(long)eleobj->ptr))
                {
                    break;
                /* else... object to object check is easy as we use the
                 * type agnostic API here. */ // 其他类型,在当前集合中查找该元素是否存在
                } else if (!setTypeIsMember(sets[j],eleobj)) {
                    break;
                }
            }
        }

        /* Only take action when all sets contain the member */
        //所有集合都带有目标元素
        if (j == setnum) {
            if (!dstkey) {// SINTER 命令,直接返回结果集元素
                if (encoding == OBJ_ENCODING_HT)
                    addReplyBulk(c,eleobj);
                else
                    addReplyBulkLongLong(c,intobj);
                cardinality++;
            } else {// SINTERSTORE 命令,将结果添加到结果集中,因为还要store到数据库中
                if (encoding == OBJ_ENCODING_INTSET) {
                    eleobj = createStringObjectFromLongLong(intobj);
                    setTypeAdd(dstset,eleobj);
                    decrRefCount(eleobj);
                } else {
                    setTypeAdd(dstset,eleobj);
                }
            }
        }
    }
    setTypeReleaseIterator(si);

    if (dstkey) { // SINTERSTORE 命令,将结果集关联到数据库
        /* Store the resulting set into the target, if the intersection
         * is not an empty set. */
        int deleted = dbDelete(c->db,dstkey);//如果之前存在该集合则先删除
        if (setTypeSize(dstset) > 0) { // 结果集大小非空,则将其添加到数据库中
            dbAdd(c->db,dstkey,dstset);//添加数据库
            addReplyLongLong(c,setTypeSize(dstset)); // 回复结果集的大小
            notifyKeyspaceEvent(NOTIFY_SET,"sinterstore",
                dstkey,c->db->id);// 发送"sinterstore"事件通知
        } else { // 结果集为空,释放空间
            decrRefCount(dstset);
            addReply(c,shared.czero);// 发送0给client
            if (deleted) // 发送"del"事件通知
                notifyKeyspaceEvent(NOTIFY_GENERIC,"del",
                    dstkey,c->db->id);
        }
        signalModifiedKey(c->db,dstkey);//发送信号
        server.dirty++;//设脏
    } else {// SINTER 命令,回复结果集给client
        setDeferredMultiBulkLength(c,replylen,cardinality);
    }
    zfree(sets); //释放集合数组空间
}

大致思路是先排序,用最小集合遍历,遍历的时候去看其他集合有无存在。时间复杂度:O(N * M), N 为给定集合当中基数最小的集合, M 为给定集合的个数。

3.2 求差集和并集命令(sdiff,sdiffstore,sunion,sunionstore)


void sunionCommand(client *c) {
    sunionDiffGenericCommand(c,c->argv+1,c->argc-1,NULL,SET_OP_UNION);
}

void sunionstoreCommand(client *c) {
    sunionDiffGenericCommand(c,c->argv+2,c->argc-2,c->argv[1],SET_OP_UNION);
}

void sdiffCommand(client *c) {
    sunionDiffGenericCommand(c,c->argv+1,c->argc-1,NULL,SET_OP_DIFF);
}

void sdiffstoreCommand(client *c) {
    sunionDiffGenericCommand(c,c->argv+2,c->argc-2,c->argv[1],SET_OP_DIFF);
}

几种运算过程都是通过sunionDiffGenericCommand函数进行,看下源码

#define SET_OP_UNION 0
#define SET_OP_DIFF 1
#define SET_OP_INTER 2

void sunionDiffGenericCommand(client *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++) {
    	// 如果dstkey为空,则是SUNION或SDIFF命令,不为空则是SUNIONSTORE或SDIFFSTORE命令
        robj *setobj = dstkey ?
            lookupKeyWrite(c->db,setkeys[j]) :
            lookupKeyRead(c->db,setkeys[j]);
        if (!setobj) { // 不存在的集合当作 NULL 来处理
            sets[j] = NULL;
            continue;
        }
        if (checkType(c,setobj,OBJ_SET)) { // 有对象不是集合,停止执行,进行清理
            zfree(sets);
            return;
        }
        sets[j] = setobj; //保存到集合数组中
    }

    /* Select what DIFF algorithm to use.
     *
     * Algorithm 1 is O(N*M) where N is the size of the element first set
     * and M the total number of sets.
     * 算法 1 的复杂度为 O(N*M) ,其中 N 为第一个集合的基数,
     * 而 M 则为其他集合的数量。
     * Algorithm 2 is O(N) where N is the total number of elements in all
     * the sets.
     *算法 2 的复杂度为 O(N) ,其中 N 为所有集合中的元素数量总数。注意:N的含义不同
     * We compute what is the best bet with the current input here. 
     *程序通过考察输入来决定使用那个算法
     */
    if (op == SET_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;//null跳过
            // 计算 setnum 乘以 sets[0] 的基数之积
            algo_one_work += setTypeSize(sets[0]);
             // 计算所有集合的基数之和
            algo_two_work += setTypeSize(sets[j]);
        }

        /* Algorithm 1 has better constant times and performs less operations
         * if there are elements in common. Give it some advantage. 
         *  我觉得这里会有人来对比下算法效率,实际测试下。
         /
        algo_one_work /= 2;
         // 算法 1 的常数比较低,优先考虑算法 1(没有直接比较两者大小选择算法, 而是算法1理论时间复杂度一半大于算法2时, 才使用算法2)
        diff_algo = (algo_one_work <= algo_two_work) ? 1 : 2;

        if (diff_algo == 1 && setnum > 1) {
            /* With algorithm 1 it is better to order the sets to subtract
             * by decreasing size, so that we are more likely to find
             * duplicated elements ASAP. */
               // 如果使用的是算法 1 ,为尽快找到重复元素,那么最好对 sets[0] 以外的其他集合进行排序
            qsort(sets+1,setnum-1,sizeof(robj*),
                qsortCompareSetsByRevCardinality);
        }
    }

    /* We need a temp set object to store our union. If the dstkey
     * is not NULL (that is, we are inside an SUNIONSTORE operation) then
     * this set object will be the resulting object to set into the target key*/
      // 创建一个临时集合对象作为结果集,如果程序执行的是 SUNIONSTORE 命令,
     *//那么这个结果将会成为将来的集合值对象。
    dstset = createIntsetObject();

    if (op == SET_OP_UNION) {/ 执行的是并集计算
        /* Union is trivial, just add every element of every set to the
         * temporary set. */
          // 遍历所有集合,将元素添加到结果集dstset里就可以了
        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) {
            	  // 讲迭代器指向的当前元素对象加入到结果集中
                if (setTypeAdd(dstset,ele)) cardinality++;  // setTypeAdd 只在集合不存在时,才会将元素添加到集合,并返回 1 
                decrRefCount(ele);
            }
            setTypeReleaseIterator(si);//释放迭代器空间
        }
    } else if (op == SET_OP_DIFF && sets[0] && diff_algo == 1) {
    	 // 执行差集操作并且使用算法1
        /* DIFF Algorithm 1:
         *
         * We perform the diff by iterating all the elements of the first set,
         * and only adding it to the target set if the element does not exist
         * into all the other sets.
         *
         * This way we perform at max N*M operations, where N is the size of
         * the first set, and M the number of sets.
         * 程序遍历 sets[0] 集合中的所有元素,
         * 并将这个元素和其他集合的所有元素进行对比,
         * 只有这个元素不存在于其他所有集合时,
         * 才将这个元素添加到结果集。
          */
        // 时间复杂度O(N*M),N是第一个集合中元素的总个数,M是集合的总个数
        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);//释放迭代器空间
    } else if (op == SET_OP_DIFF && sets[0] && diff_algo == 2) {
    	// 执行差集操作并且使用算法2
        /* DIFF Algorithm 2:
         *
         * Add all the elements of the first set to the auxiliary set.
         * Then remove all the elements of all the next sets from it.
         * 将 sets[0] 的所有元素都添加到结果集中,
         * 然后遍历其他所有集合,将相同的元素从结果集中删除。
         * This is O(N) where N is the sum of all the elements in every
         *  算法复杂度为 O(N) ,N 为所有集合的基数之和。
         * 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) {
                if (j == 0) {// sets[0] 时,将所有元素添加到集合
                    if (setTypeAdd(dstset,ele)) cardinality++;
                } else {// 不是 sets[0] 时,其他集合中元素出现在dstset中,则删除该元素
                    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;
        }
    }

    /* Output the content of the resulting set, if not in STORE mode */
    if (!dstkey) {// 执行的是 SDIFF 或者 SUNION  ,不需要存储的命令
        addReplyMultiBulkLen(c,cardinality); // 发送结果集的元素个数给client
        si = setTypeInitIterator(dstset);
        // 遍历结果集中的每一个元素,并发送给client
        while((ele = setTypeNextObject(si)) != NULL) {
            addReplyBulk(c,ele);
            decrRefCount(ele);//发送完后面要释放空间
        }
        setTypeReleaseIterator(si);
        decrRefCount(dstset);
    } else {// 执行的是 SDIFFSTORE 或者 SUNIONSTORE
        /* If we have a target key where to store the resulting set
         * create this key with the result set inside */
          /* 需要存储, 首先删除原来可能已经存在dstkey的集合*/
        int deleted = dbDelete(c->db,dstkey);
        if (setTypeSize(dstset) > 0) { // 如果结果集不为空,将它保存到数据库中
            dbAdd(c->db,dstkey,dstset); //将结果集加入到数据库中
            addReplyLongLong(c,setTypeSize(dstset));//发送结果集的元素个数给client
            notifyKeyspaceEvent(NOTIFY_SET,
                op == SET_OP_UNION ? "sunionstore" : "sdiffstore",
                dstkey,c->db->id); // 发送对应的事件通知 sunionstore or sdiffstore
        } else {// 结果集为空,释放空间
            decrRefCount(dstset);
            addReply(c,shared.czero);//发送0给client
            if (deleted) //发送del通知
                notifyKeyspaceEvent(NOTIFY_GENERIC,"del",
                    dstkey,c->db->id);
        }
        signalModifiedKey(c->db,dstkey);
        server.dirty++;//更新脏键
    }
    zfree(sets);//释放集合数组空间
}

主要步骤:判断操作类型,对于求并集直接遍历添加。对于求差集,有两证算法:   

  • 算法 1 的复杂度为 O(N*M) ,其中 N 为第一个集合的基数, 而 M 则为其他集合的数量。
  • 算法 2 的复杂度为 O(N) ,其中 N 为所有集合中的元素数量总数。注意:N的含义不同

最后,判断是否存储,不存储直接返回,存储保存到db。

时间关系没有去验证,我觉得会有人对算法进行验证的。

 

参数:

https://blog.csdn.net/men_wen/article/details/70941408

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值