redis源码分析与思考(十六)——集合类型的命令实现(t_set.c)

版权声明:博主GitHub地址https://github.com/suyeq欢迎大家前来交流学习 https://blog.csdn.net/hackersuye/article/details/83117515

    集合类型是用来保存多个字符串的,与列表类型不一样,集合中不允许有重复的元素,也不能以索引的方式来通过下标获取值,集合中的元素还是无序的。在普通的集合上增删查改外,集合类型还实现了多个集合的取交集、并集、差集,集合的命令如下表所示:

集合命令表

命令 对应操作 时间复杂度
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,ele);
            decrRefCount(ele);
        }
        setTypeReleaseIterator(si);
        decrRefCount(dstset);
    // 执行的是 SDIFFSTORE 或者 SUNIONSTORE
    } else {
        // 现删除现在可能有的 dstkey
        int deleted = dbDelete(c->db,dstkey);
        // 如果结果集不为空,将它关联到数据库中
        if (setTypeSize(dstset) > 0) {
            dbAdd(c->db,dstkey,dstset);
            // 返回结果集的基数
            addReplyLongLong(c,setTypeSize(dstset));
            notifyKeyspaceEvent(REDIS_NOTIFY_SET,
                op == REDIS_OP_UNION ? "sunionstore" : "sdiffstore",
                dstkey,c->db->id);
        // 结果集为空
        } else {
            decrRefCount(dstset);
            // 返回 0 
            addReply(c,shared.czero);
            if (deleted)
                notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",
                    dstkey,c->db->id);
        }
        signalModifiedKey(c->db,dstkey);
        server.dirty++;
    }
    zfree(sets);
}

猜你喜欢

转载自blog.csdn.net/hackersuye/article/details/83117515