1

顺风车运营研发团队 熊浩含

sdiff
SDIFF key [key ...]

返回一个集合的全部成员,该集合是所有给定集合之间的差集。

准确来说,是返回第一个集合与其它集合并集的差集,即有SDIFF A B1 B2 B3...Bn,最终返回A-(B1∪B2∪...∪Bn)

不存在的 key 被视为空集。

原理
M(A 集合元素个数)

N(其余集合元素总数)

n(集合总数)

算法一

将A集合元素在B1...Bn集合间一一查找,将查找不到的元素加入结果集。复杂度=M*n

算法二

先将A集合加入结果集,再将B1...Bn集合中的所有元素在结果集中一一查找,将查到的元素从结果集中删除。复杂度=N

在算法选择时,大的逻辑是:如果A集合比较小,遍历A更划算;反之遍历其余集合更划算

源码

void sunionDiffGenericCommand(client *c, robj **setkeys, int setnum,robj *dstkey, int op) {
    robj **sets = zmalloc(sizeof(robj*)*setnum);//sets是集合指针的指针,setnum是集合总数
    setTypeIterator *si;//迭代器
    robj *dstset = NULL;//目标集合,最终返回
    sds ele;
    int j, cardinality = 0;//临时变量
    int diff_algo = 1;//算法标识
 
    for (j = 0; j < setnum; j++) {//循环keys,把取到的set放入sets中
        robj *setobj = dstkey ?
            lookupKeyWrite(c->db,setkeys[j]) :
            lookupKeyRead(c->db,setkeys[j]);
        if (!setobj) {//取出set,如果key不存在,跳过
            sets[j] = NULL;
            continue;
        }
        if (checkType(c,setobj,OBJ_SET)) {//校验obj的类型,如果不为OBG_SET,错误,直接返回
            zfree(sets);
            return;
        }
        sets[j] = setobj;//把取到的set放入sets中
    }
 
    /* 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.
     *
     * Algorithm 2 is O(N) where N is the total number of elements in all
     * the sets.
     *
     * 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;
            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;
        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 == SET_OP_UNION) {
        /* 遍历所有集合,将元素添加到结果集里就可以了*/
        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++;
                sdsfree(ele);
            }
            setTypeReleaseIterator(si);
        }
    } else if (op == SET_OP_DIFF && sets[0] && diff_algo == 1) {
        /* DIFF Algorithm 1:
         *程序遍历 sets[0] 集合中的所有元素,并将这个元素和其他集合的所有元素进行对比,只有这个元素不存在于其他所有集合时,才将这个元素添加到结果集。
         * 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++;
            }
            sdsfree(ele);
        }
        setTypeReleaseIterator(si);
    } else if (op == SET_OP_DIFF && sets[0] && diff_algo == 2) {
        /* DIFF Algorithm 2:
         * 将 sets[0] 的所有元素都添加到结果集中,然后遍历其他所有集合,将相同的元素从结果集中删除。
         * 算法复杂度为 O(N) ,N 为所有集合的基数之和。
        */
        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) {
                    if (setTypeAdd(dstset,ele)) cardinality++;
                } else {
                    if (setTypeRemove(dstset,ele)) cardinality--;
                }
                sdsfree(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) {
        addReplyMultiBulkLen(c,cardinality);
        si = setTypeInitIterator(dstset);
        while((ele = setTypeNextObject(si)) != NULL) {
            addReplyBulkCBuffer(c,ele,sdslen(ele));
            sdsfree(ele);
        }
        setTypeReleaseIterator(si);
        decrRefCount(dstset);
    } else {
        /* If we have a target key where to store the resulting set
         * create this key with the result set inside */
        int deleted = dbDelete(c->db,dstkey);
        if (setTypeSize(dstset) > 0) {
            dbAdd(c->db,dstkey,dstset);
            addReplyLongLong(c,setTypeSize(dstset));
            notifyKeyspaceEvent(NOTIFY_SET,
                op == SET_OP_UNION ? "sunionstore" : "sdiffstore",
                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++;
    }
    zfree(sets);
}
这里有个疑问,为啥在算法一中,redis将B1...Bn集合按元素个数从大到小排序?

一种解释是:查询A中元素是否在其余集合中时,大集合更容易命中,命中后就不用遍历其余集合了,提升了效率。

但由于每次dictfind都是o(1)的复杂度,这样排一下序效率提升很有限,而且排序本身也有额外的花销,感觉没有必要。

sunion
SUNION key [key ...]

返回一个集合的全部成员,该集合是所有给定集合的并集。

不存在的 key 被视为空集。

原理
将所有集合依次插入结果集,插一遍自然去重,返回结果集即可

源码
sunion底层调用的方法和sdiff一样,详见sdiff。

sinter
SINTER key [key ...]

返回一个集合的全部成员,该集合是所有给定集合的交集。

不存在的 key 被视为空集。

当给定集合当中有一个空集时,结果也为空集(根据集合运算定律)。

原理
找到元素个数最小的集合A
将A中元素一一遍历其它集合,若其它集合都包含该元素,保留该元素
源码

void sinterGenericCommand(client *c, robj **setkeys,unsigned long setnum, robj *dstkey) {
    robj **sets = zmalloc(sizeof(robj*)*setnum);
    setTypeIterator *si;
    robj *dstset = NULL;
    sds elesds;
    int64_t intobj;
    void *replylen = NULL;
    unsigned long j, cardinality = 0;
    int encoding;
 
    for (j = 0; j < setnum; j++) {
        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,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 */
    /*遍历第一个集合A,并在其它集合中查询A中的所有元素,对于A中的元素,只要有一个集合中不存在该元素,删去该元素*/
    si = setTypeInitIterator(sets[0]);
    while((encoding = setTypeNext(si,&elesds,&intobj)) != -1) {
        for (j = 1; j < setnum; j++) {
            if (sets[j] == sets[0]) continue;
            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) {
                if (!setTypeIsMember(sets[j],elesds)) {
                    break;
                }
            }
        }
 
        /* Only take action when all sets contain the member */
        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);
 
    if (dstkey) {
        /* 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);
        } else {
            decrRefCount(dstset);
            addReply(c,shared.czero);
            if (deleted)
                notifyKeyspaceEvent(NOTIFY_GENERIC,"del",
                    dstkey,c->db->id);
        }
        signalModifiedKey(c->db,dstkey);
        server.dirty++;
    } else {
        setDeferredMultiBulkLength(c,replylen,cardinality);
    }
    zfree(sets);
}

AI及LNMPRG研究
7.2k 声望12.8k 粉丝

一群热爱代码的人 研究Nginx PHP Redis Memcache Beanstalk 等源码 以及一群热爱前端的人