Redis source code analysis and annotation: set type key implementation (t_set)

Redis set type key implementation

  1. Set command introduction
    All the set commands in redis are as follows: redis set command details

  2. Implementation of collection type
    As mentioned in the source code analysis and annotation of redis object system, there are two types of object codes for a collection type, namely obj ﹣ encoding ﹣ HT and obj ﹣ encoding ﹣ intset.


For the source code analysis and annotation of the two data structures at the bottom of the collection type, see the following blog.

Analysis and annotation of Redis dictionary structure source code

Redis integer set source code analysis and comments

The conditions for conversion from obj? Encoding? Intset to obj? Encoding? HT are as follows:

Options in redis's configuration file: if the number of elements of a collection object whose data is encoded as an integer collection exceeds the set Max intset entries threshold, the encoding is converted

set-max-intset-entries  512

If you insert an object of type string into a collection object whose data is encoded as an integer collection, the encoding is converted
The source code of the data code conversion of the collection object is as follows:

//Convert INTSET encoding type of set object to enc type

void setTypeConvert(robj *setobj, int enc) {
    setTypeIterator *si;
    serverAssertWithInfo(NULL,setobj,setobj->type == OBJ_SET &&
                             setobj->encoding == OBJ_ENCODING_INTSET);

    // Code converted to obj? Encoding? HT dictionary type
    if (enc == OBJ_ENCODING_HT) {
        int64_t intele;
        // Create a dictionary
        dict *d = dictCreate(&setDictType,NULL);
        robj *element;

        /* Presize the dict to avoid rehashing */
        // Expand the size of the dictionary
        dictExpand(d,intsetLen(setobj->ptr));

        /* To add the elements we extract integers and create redis objects */
        // Create and initialize an iterator of collection type
        si = setTypeInitIterator(setobj);
        // Iterator integer set
        while (setTypeNext(si,&element,&intele) != -1) {
            element = createStringObjectFromLongLong(intele);   //Converts elements in the current collection to string type objects
            serverAssertWithInfo(NULL,element,
                                dictAdd(d,element,NULL) == DICT_OK);
        }
        // Free iterator space
        setTypeReleaseIterator(si);

        // Set the encoding type of the converted collection object
        setobj->encoding = OBJ_ENCODING_HT;
        // Update the value object of the collection object
        zfree(setobj->ptr);
        setobj->ptr = d;
    } else {
        serverPanic("Unsupported set conversion");
    }
}

The structure of a collection object is defined as follows:

typedef struct redisObject {
    //The data type of the object. The collection object should be obj set
    unsigned type:4;        
    //The encoding type of the object, obj? Encoding? Intset or obj? Encoding? HT
    unsigned encoding:4;
    //Don't care about the member for the time being
    unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
    //Reference counting
    int refcount;
    //Pointer to the underlying data implementation, to a dict's dictionary structure or integer set structure
    void *ptr;
} robj;

Let's assume that a set holds age tags, 1995, 1996, 1997, 1994

127.0.0.1:6379> SADD tags:age 1997 1995 1994 1996
(integer) 4
127.0.0.1:6379> OBJECT ENCODING tags:age
"intset"
127.0.0.1:6379> SMEMBERS tags:age
1) "1994"
2) "1995"
3) "1996"
4) "1997"

The spatial structure of the tags:age collection object may be as follows:

Let's say that a collection holds the label of a movement, including basketball, football and volleyball

127.0.0.1:6379> SADD tags:sport basketball football volleyball
(integer) 3
127.0.0.1:6379> OBJECT ENCODING tags:sport
"hashtable"
127.0.0.1:6379> SMEMBERS tags:sport
1) "volleyball"
2) "basketball"
3) "football"

The spatial structure of this tags:sport collection object may be as follows:


API encapsulated by collection type

/* Set data type */
// Create a collection of value s
robj *setTypeCreate(robj *value);
// Add value to the subject collection. If it is added successfully, 1 will be returned. If it already exists, 0 will be returned
int setTypeAdd(robj *subject, robj *value);
// Delete an element with value from the collection object. 1 will be returned if the deletion succeeds and 0 will be returned if the deletion fails
int setTypeRemove(robj *subject, robj *value);
// Whether there is an element with value in the collection. If there is one, return 1. Otherwise, return 0
int setTypeIsMember(robj *subject, robj *value);
// Create and initialize an iterator of collection type
setTypeIterator *setTypeInitIterator(robj *subject);
// Free iterator space
void setTypeReleaseIterator(setTypeIterator *si);
// Save the element pointed by the current iterator in obele or LLE, and return - 1 after iteration
// The reference technology of the returned object is not increased, and it supports read-time sharing and write time copying
int setTypeNext(setTypeIterator *si, robj **objele, int64_t *llele);
// Returns the address of the element object that the iterator currently points to. The returned object needs to be released manually
robj *setTypeNextObject(setTypeIterator *si);
// Take an object randomly from the collection and save it in the parameter
int setTypeRandomElement(robj *setobj, robj **objele, int64_t *llele);
unsigned long setTypeRandomElements(robj *set, unsigned long count, robj *aux_set);
// Returns the number of elements in the collection
unsigned long setTypeSize(robj *subject);
// Convert INTSET encoding type of set object to enc type
void setTypeConvert(robj *subject, int enc);

All API comments: collection type implementation source code comments

The collection type implements its own iterator, which is also encapsulated by the dictionary based iterator

/* Structure to hold set iteration abstraction. */
typedef struct {
    robj *subject;                  //Collection object to which
    int encoding;                   //Set object encoding type
    int ii; /* intset iterator */   //Iterator of integer set, encoded for INTSET use
    dictIterator *di;               //Iterator of dictionary, encoded for HT use
} setTypeIterator;

Operation of iterators of collection type:

Create and initialize an iterator of collection type
//Create and initialize an iterator of collection type

setTypeIterator *setTypeInitIterator(robj *subject) {
    // Allocate space and initialize members
    setTypeIterator *si = zmalloc(sizeof(setTypeIterator));
    si->subject = subject;
    si->encoding = subject->encoding;

    // Iterator of initialization dictionary
    if (si->encoding == OBJ_ENCODING_HT) {
        si->di = dictGetIterator(subject->ptr);

    // Initializes the iterator of the collection, which is the subscript of the collection
    } else if (si->encoding == OBJ_ENCODING_INTSET) {
        si->ii = 0;
    } else {
        serverPanic("Unknown set encoding");
    }
    return si;
}

Release iterator of collection type

// Free iterator space
void setTypeReleaseIterator(setTypeIterator *si) {
    // If it is a dictionary type, you need to release the iterator of the dictionary type first
    if (si->encoding == OBJ_ENCODING_HT)
        dictReleaseIterator(si->di);
    zfree(si);
}

There are two types of iterative operations
Save the object pointed to by the current iterator in the parameter, and support read-time share and write time copy: setTypeNext() function
Take the object pointed to by the current iterator as the return value. Read time sharing and write time replication are not supported. The returned object needs to be released: setTypeNextObject() function

// Save the element pointed by the current iterator in obele or LLE, and return - 1 after iteration
// The reference count of the returned object does not increase. It supports read-time sharing and write time copying
int setTypeNext(setTypeIterator *si, robj **objele, int64_t *llele) {
    // Iterative dictionary
    if (si->encoding == OBJ_ENCODING_HT) {
        // Get the next node address, update the iterator
        dictEntry *de = dictNext(si->di);
        if (de == NULL) return -1;
        // Preserving elements
        *objele = dictGetKey(de);
        *llele = -123456789; /* Not needed. Defensive. */
    // Iterated integer set
    } else if (si->encoding == OBJ_ENCODING_INTSET) {
        // Save elements from intset to LLE
        if (!intsetGet(si->subject->ptr,si->ii++,llele))
            return -1;
        *objele = NULL; /* Not needed. Defensive. */
    } else {
        serverPanic("Wrong set encoding in setTypeNext");
    }
    return si->encoding;    //Return code type
}

// Returns the address of the element object that the iterator currently points to. The returned object needs to be released manually
robj *setTypeNextObject(setTypeIterator *si) {
    int64_t intele;
    robj *objele;
    int encoding;

    // Get the encoding type of the current collection object
    encoding = setTypeNext(si,&objele,&intele);
    switch(encoding) {
        case -1:    return NULL;    //Iterative completion
        case OBJ_ENCODING_INTSET:   //Integer collection returns an object of type string
            return createStringObjectFromLongLong(intele);
        case OBJ_ENCODING_HT:       //Dictionary collection, which returns the shared object
            incrRefCount(objele);
            return objele;
        default:
            serverPanic("Unsupported encoding");
    }
    return NULL; /* just to suppress warnings */
}
  1. Implementation of set key command
    The command of gathering keys mostly depends on the encoding to determine the data type, and then calls the corresponding data structure API to achieve. However, there are several key points of the set key command. We do not have any analysis. Please check the source code annotation on github: redis collection key command source code annotation.

Bottom implementation of intersection command

// SINTER key [key ...]
// SINTERSTORE destination key [key ...]
// The underlying implementation of command such as SINTER and SINTERSTORE
void sinterGenericCommand(client *c, robj **setkeys,
                          unsigned long setnum, robj *dstkey) {
    // Assign an array of storage collections
    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;

    // Traversal collection array
    for (j = 0; j < setnum; j++) {
        // If dstkey is empty, it is the SINTER command. If it is not empty, it is the SINTERSTORE command
        // If it is the SINTER command, the collection object is read out by read operation; otherwise, the collection object is read out by write operation
        robj *setobj = dstkey ?
            lookupKeyWrite(c->db,setkeys[j]) :
            lookupKeyRead(c->db,setkeys[j]);

        // Read collection object does not exist, perform cleanup operation
        if (!setobj) {
            zfree(sets);    //Free collection array space
            // If it is the SINTERSTORE command
            if (dstkey) {
                // Delete the stored target collection object dstkey from the database
                if (dbDelete(c->db,dstkey)) {
                    // Signals that the database key is modified and the dirty key is updated
                    signalModifiedKey(c->db,dstkey);
                    server.dirty++;
                }
                addReply(c,shared.czero);   //Send 0 to client
            // If it is the SINTER command, send a blank reply
            } else {
                addReply(c,shared.emptymultibulk);
            }
            return;
        }

        // Read collection object successfully, check its data type
        if (checkType(c,setobj,OBJ_SET)) {
            zfree(sets);
            return;
        }
        // Save the read object in the collection array
        sets[j] = setobj;
    }
    /* Sort sets from the smallest to largest, this will improve our
     * algorithm's performance */
    // Sorting the set size in the set array from small to large can improve the performance of the algorithm
    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 */
    // First we should output the number of elements in the collection, but now we don't know the size of the intersection
    // So create a list of empty objects and save all the replies
    if (!dstkey) {
        replylen = addDeferredMultiBulkLength(c);   // The STINER command creates a linked list
    } else {
        /* If we have a target key where to store the resulting set
         * create this key with an empty set inside */
        dstset = createIntsetObject();              //The steinerstore command creates an object to be set to an integer
    }

    /* 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 */
    // Iterate over each element of the first and smallest set, and compare all elements in the set with other sets
    // If at least one collection does not include the element, it is not an intersection
    si = setTypeInitIterator(sets[0]);
    // Create an iterator of collection type and iterate all elements of the first collection in the collection array
    while((encoding = setTypeNext(si,&eleobj,&intobj)) != -1) {
        // Traverse other collections
        for (j = 1; j < setnum; j++) {

            // Skipping a set that is equal to the first set, there is no need to compare the elements of two identical sets, and the first set is the intersection of the results
            if (sets[j] == sets[0]) continue;
            // The current element is of type INTSET
            if (encoding == OBJ_ENCODING_INTSET) {
                /* intset with intset is simple... and fast */
                // If the element is not found in the current intset collection, skip the current element directly and operate on the next element
                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 */
                // Find in dictionary
                } else if (sets[j]->encoding == OBJ_ENCODING_HT) {
                    // Create string object
                    eleobj = createStringObjectFromLongLong(intobj);
                    // If the current element is not an element in the current collection, release the string object to skip the for loop body and operate on the next element
                    if (!setTypeIsMember(sets[j],eleobj)) {
                        decrRefCount(eleobj);
                        break;
                    }
                    decrRefCount(eleobj);
                }
            // Current element is of type HT dictionary
            } 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. */
                // The encoding of the current element is of type int and the current set is an integer set. If the set does not contain the element, skip the loop
                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. */
                // Other types, find whether the element exists in the current collection
                } else if (!setTypeIsMember(sets[j],eleobj)) {
                    break;
                }
            }
        }

        /* Only take action when all sets contain the member */
        // Executed here, which is the element in the result set
        if (j == setnum) {
            // If it is the SINTER command, reply to the set
            if (!dstkey) {
                if (encoding == OBJ_ENCODING_HT)
                    addReplyBulk(c,eleobj);
                else
                    addReplyBulkLongLong(c,intobj);
                cardinality++;

            // If it is the SINTERSTORE command, first add the result to the collection, because you also need to store it to the database
            } else {
                if (encoding == OBJ_ENCODING_INTSET) {
                    eleobj = createStringObjectFromLongLong(intobj);
                    setTypeAdd(dstset,eleobj);
                    decrRefCount(eleobj);
                } else {
                    setTypeAdd(dstset,eleobj);
                }
            }
        }
    }
    setTypeReleaseIterator(si); //Release iterator

    // SINTERSTORE command to add a collection of results to the database
    if (dstkey) {
        /* Store the resulting set into the target, if the intersection
         * is not an empty set. */
        // Delete the collection if it exists before
        int deleted = dbDelete(c->db,dstkey);
        // If the result set size is not empty, it will be added to the database
        if (setTypeSize(dstset) > 0) {
            dbAdd(c->db,dstkey,dstset);
            // Size of reply result set
            addReplyLongLong(c,setTypeSize(dstset));
            // Send "singlestore" event notification
            notifyKeyspaceEvent(NOTIFY_SET,"sinterstore",
                dstkey,c->db->id);
        // Empty result set, free space
        } else {
            decrRefCount(dstset);
            // Send 0 to client
            addReply(c,shared.czero);
            // Send "del" event notification
            if (deleted)
                notifyKeyspaceEvent(NOTIFY_GENERIC,"del",
                    dstkey,c->db->id);
        }
        // The key is modified to send a signal. Update dirty key
        signalModifiedKey(c->db,dstkey);
        server.dirty++;

    // SINTER command, reply result set to client
    } else {
        setDeferredMultiBulkLength(c,replylen,cardinality);
    }
    zfree(sets);    //Free collection array space
}

The bottom implementation of difference and union commands
Two algorithms are given to calculate the difference set, which are used in different scenes.

Time complexity O(N*M), N is the total number of elements in the first set, M is the total number of sets
Time complexity O(N), N is the total number of elements in all sets
#Define set? OP? Union 0 / / Union
#Define set? OP? Diff 1 / / difference set
#Define set? OP? Inter 2 / / intersection

// SUNION key [key ...]
// SUNIONSTORE destination key [key ...]
// SDIFF key [key ...]
// SDIFFSTORE destination key [key ...]
// The bottom implementation of Union and difference commands
void sunionDiffGenericCommand(client *c, robj **setkeys, int setnum,
                              robj *dstkey, int op) {
    //Allocate space for collection arrays
    robj **sets = zmalloc(sizeof(robj*)*setnum);
    setTypeIterator *si;
    robj *ele, *dstset = NULL;
    int j, cardinality = 0;
    int diff_algo = 1;

    // Traversing set key objects in an array
    for (j = 0; j < setnum; j++) {
        // If dstkey is empty, it is the sun on or SDIFF command; otherwise, it is the sun store or SDIFFSTORE command
        // If it is the sun on or SDIFF command, read and fetch the collection object with read operation, otherwise read and fetch the collection object with write operation
        robj *setobj = dstkey ?
            lookupKeyWrite(c->db,setkeys[j]) :
            lookupKeyRead(c->db,setkeys[j]);
        // Nonexistent set key is set to null
        if (!setobj) {
            sets[j] = NULL;
            continue;
        }
        // Check whether the existing collection key is a collection object, otherwise, free space
        if (checkType(c,setobj,OBJ_SET)) {
            zfree(sets);
            return;
        }
        sets[j] = setobj;   //Save to collection array
    }

    /* 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. */
    // There are two algorithms for calculating difference sets
    // 1. Time complexity O(N*M), N is the total number of elements in the first set, M is the total number of sets
    // 2. Time complexity O(N), N is the total number of elements in all sets
    if (op == SET_OP_DIFF && sets[0]) {
        long long algo_one_work = 0, algo_two_work = 0;

        // Traversal collection array
        for (j = 0; j < setnum; j++) {
            if (sets[j] == NULL) continue;

            // Calculate the value of sets[0] × setnum
            algo_one_work += setTypeSize(sets[0]);
            // Calculate the total number of elements in all sets
            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;
        //Select different algorithms according to algo one work and algo two work
        diff_algo = (algo_one_work <= algo_two_work) ? 1 : 2;

        // If it is algorithm 1, M is smaller and less operations are performed
        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. */
            // Sort all collections except the first collection by the elements of the collection
            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*/
    // Create a temporary collection object as the result set
    dstset = createIntsetObject();

    // Perform union operation
    if (op == SET_OP_UNION) {
        /* Union is trivial, just add every element of every set to the
         * temporary set. */
        // Let's just say that every element in every set is added to the result set
        // Traverse each set
        for (j = 0; j < setnum; j++) {
            if (!sets[j]) continue; /* non existing keys are like empty sets */

            // Create an iterator of collection type
            si = setTypeInitIterator(sets[j]);
            // Traverse all elements in the current collection
            while((ele = setTypeNextObject(si)) != NULL) {
                // Adding the current element object pointed by the iterator to the result set
                if (setTypeAdd(dstset,ele)) cardinality++;  //If there are no newly added elements in the result set, update the element count counter of the result set
                decrRefCount(ele);  //Otherwise, release the element object space directly
            }
            setTypeReleaseIterator(si);     //Free iterator space
        }
    // Perform a subtraction operation and use algorithm 1
    } else if (op == SET_OP_DIFF && sets[0] && diff_algo == 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. */
        // We perform a subtraction operation by traversing all the elements in the first set and adding elements that do not exist in other sets to the result set
        // Time complexity O(N*M), N is the total number of elements in the first set, M is the total number of sets
        si = setTypeInitIterator(sets[0]);
        // Create a collection type iterator to traverse all elements in the first collection
        while((ele = setTypeNextObject(si)) != NULL) {
            // Traverse all but the first collection in the collection array to check whether the element exists in each collection
            for (j = 1; j < setnum; j++) {
                if (!sets[j]) continue; /* no key is an empty set. */   //Set key does not exist skip this cycle
                if (sets[j] == sets[0]) break; /* same set! */          //There's no need to compare the same set
                if (setTypeIsMember(sets[j],ele)) break;                //If the element exists in the following collection, traverse to the next element
            }
            // Executed here to indicate that the current element does not exist in all collections except the first
            if (j == setnum) {
                /* There is no other set with this element. Add it. */
                // So add the current element to the result set and update the counter
                setTypeAdd(dstset,ele);
                cardinality++;
            }
            decrRefCount(ele);  //Free element object space
        }
        setTypeReleaseIterator(si); //Free iterator space
    // Perform a subtraction operation and use algorithm 2
    } else if (op == SET_OP_DIFF && sets[0] && diff_algo == 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.
         *
         * This is O(N) where N is the sum of all the elements in every
         * set. */
        // Add all elements of the first set to the result set, then traverse all subsequent sets, and delete elements with intersection from the result set
        // 2. Time complexity O(N), N is the total number of elements in all sets
        // Traverse all sets
        for (j = 0; j < setnum; j++) {
            if (!sets[j]) continue; /* non existing keys are like empty sets */

            si = setTypeInitIterator(sets[j]);
            // Create a collection type iterator to traverse all elements in each collection
            while((ele = setTypeNextObject(si)) != NULL) {
                // If it is the first set, add each element to the result set
                if (j == 0) {
                    if (setTypeAdd(dstset,ele)) cardinality++;
                // If it is a subsequent set, delete the current element from the result set, as there is in the result set
                } else {
                    if (setTypeRemove(dstset,ele)) cardinality--;
                }
                decrRefCount(ele);
            }
            setTypeReleaseIterator(si);//Free iterator space

            /* Exit if result set is empty as any additional removal
             * of elements will have no effect. */
            // As long as the result set is empty, the result of the difference set will be empty, and subsequent sets will not be compared
            if (cardinality == 0) break;
        }
    }

    /* Output the content of the resulting set, if not in STORE mode */
    // If it's not a command like STORE, output all the results
    if (!dstkey) {
        // Send the number of elements in the result set to the client
        addReplyMultiBulkLen(c,cardinality);

        // Traverse each element in the result set and send it to the client
        si = setTypeInitIterator(dstset);
        while((ele = setTypeNextObject(si)) != NULL) {
            addReplyBulk(c,ele);
            decrRefCount(ele);  //Free space after sending
        }
        setTypeReleaseIterator(si); //Release iterator
        decrRefCount(dstset);       //Space to free result set after sending collection

    // STORE command, output all results
    } else {
        /* If we have a target key where to store the resulting set
         * create this key with the result set inside */
        // Remove the target collection from the database first, if it exists
        int deleted = dbDelete(c->db,dstkey);
        // If the result set is not empty
        if (setTypeSize(dstset) > 0) {
            dbAdd(c->db,dstkey,dstset); //Add result set to database
            addReplyLongLong(c,setTypeSize(dstset));    //Send the number of elements in the result set to the client
            // Send corresponding event notification
            notifyKeyspaceEvent(NOTIFY_SET,
                op == SET_OP_UNION ? "sunionstore" : "sdiffstore",
                dstkey,c->db->id);

        // Empty result set, free space
        } else {
            decrRefCount(dstset);
            addReply(c,shared.czero);   //Send 0 to client
            // Send "del" event notification
            if (deleted)
                notifyKeyspaceEvent(NOTIFY_GENERIC,"del",
                    dstkey,c->db->id);
        }
        // Key modified, send signal notification, update dirty key
        signalModifiedKey(c->db,dstkey);
        server.dirty++;
    }
    zfree(sets);    //Free collection array space
}
Published 52 original articles, won praise 9, visited 30000+
Private letter follow

Tags: encoding Redis Database less

Posted on Mon, 13 Jan 2020 19:25:53 -0800 by anindya23