Redis源码阅读之zdel

Redis源码阅读之zdel

June 25, 2024
Redis
Redis
Redis版本:7.0.2

zremCommand #

zrem命令对应的函数为zremCommand,其定义如下:

/***************t_zset.c****************/
void zremCommand(client *c) {
    robj *key = c->argv[1];
    robj *zobj;
    int deleted = 0, keyremoved = 0, j;

    if ((zobj = lookupKeyWriteOrReply(c,key,shared.czero)) == NULL ||
        checkType(c,zobj,OBJ_ZSET)) return;

    for (j = 2; j < c->argc; j++) {
        if (zsetDel(zobj,c->argv[j]->ptr)) deleted++;
        if (zsetLength(zobj) == 0) {
            dbDelete(c->db,key);
            keyremoved = 1;
            break;
        }
    }

    if (deleted) {
        notifyKeyspaceEvent(NOTIFY_ZSET,"zrem",key,c->db->id);
        if (keyremoved)
            notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key,c->db->id);
        signalModifiedKey(c,c->db,key);
        server.dirty += deleted;
    }
    addReplyLongLong(c,deleted);
}

主要逻辑为:

  1. 从数据库中查找key对应的zset对象,如果不存在则返回。如果类型不正确也返回。
  2. 遍历所有的member,删除zset中的member。如果删除后zset为空,则删除key。
  3. 如果删除的member数量不为0,则发送事件通知,更新脏键。
  4. 返回删除的member数量。

我们下面着重看一下zsetDel函数的实现。

zsetDel-删除有序集合中的成员 #

zsetDel函数的定义如下:

/*****************************t_zset.c*******************************/

/* Delete the element 'ele' from the sorted set, returning 1 if the element
 * existed and was deleted, 0 otherwise (the element was not there).
 * 删除有序集合中的元素'ele',如果元素存在且被删除,则返回1,否则返回0。
 */ 
*/
int zsetDel(robj *zobj, sds ele) {
    if (zobj->encoding == OBJ_ENCODING_LISTPACK) {
        unsigned char *eptr;

        if ((eptr = zzlFind(zobj->ptr,ele,NULL)) != NULL) {
            zobj->ptr = zzlDelete(zobj->ptr,eptr);
            return 1;
        }
    } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
        zset *zs = zobj->ptr;
        if (zsetRemoveFromSkiplist(zs, ele)) {
            if (htNeedsResize(zs->dict)) dictResize(zs->dict);
            return 1;
        }
    } else {
        serverPanic("Unknown sorted set encoding");
    }
    return 0; /* No such element found. */
}

这个函数的主要逻辑为:

  1. 如果zset的编码为OBJ_ENCODING_LISTPACK,则调用zzlFind函数查找元素,如果找到则调用zzlDelete删除元素。
  2. 如果zset的编码为OBJ_ENCODING_SKIPLIST,则调用zsetRemoveFromSkiplist函数删除元素。
  3. 移除元素后,如果哈希表需要缩容(有序集合使用跳表编码时实际上是跳表+哈希表存储),则调用dictResize函数进行缩容。
  4. 如果编码不正确,则报错。

我们下面再来看一下zsetRemoveFromSkiplist函数的实现。

zsetRemoveFromSkiplist-使用跳表编码时怎样删除成员 #

使用跳表(skiplist)编码时,会调用zsetRemoveFromSkiplist来移除成员,源码如下:

/***********************z_set.c****************************/
/* Deletes the element 'ele' from the sorted set encoded as a skiplist+dict,
 * returning 1 if the element existed and was deleted, 0 otherwise (the
 * element was not there). It does not resize the dict after deleting the
 * element. 
 * 从以跳表+字典编码的有序集合中删除元素'ele',如果元素存在且被删除,则返回1,否则返回0。
 * 删除元素后不会调整字典的大小(应该说本函数不会调整,但是会在函数外调整)。
 * */
static int zsetRemoveFromSkiplist(zset *zs, sds ele) {
    dictEntry *de;
    double score;

    de = dictUnlink(zs->dict,ele);
    if (de != NULL) {
        /* Get the score in order to delete from the skiplist later. 
        *  获取分数,以便稍后从跳表中删除。
        */
        score = *(double*)dictGetVal(de);

        /* Delete from the hash table and later from the skiplist.
         * Note that the order is important: deleting from the skiplist
         * actually releases the SDS string representing the element,
         * which is shared between the skiplist and the hash table, so
         * we need to delete from the skiplist as the final step. 
         * 先从哈希表中删除,然后从跳表中删除。
         * 注意这个顺序很重要:从跳表中删除实际上会释放代表元素的SDS字符串,这个字符串在跳表和哈希表之间共享,
         * 所以我们需要在最后一步才从跳表中删除。
         * */
        dictFreeUnlinkedEntry(zs->dict,de);

        /* Delete from skiplist. 
        * 从跳表中删除。
        */
        int retval = zslDelete(zs->zsl,score,ele,NULL);
        serverAssert(retval);

        return 1;
    }

    return 0;
}

其主要逻辑为:

  1. 从字典中删除元素,如果元素存在,则获取元素的分数。
  2. 释放字典中的元素。
  3. 从跳表中删除元素。

zslDelete-究竟怎样从一个跳表中删除元素 #

跳表编码下,真正从跳表中移除元素时,调用的是zslDelete,在这个函数中会完成跳表查找,移除成员/节点的操作。 zslDelete函数的源码如下:

/**************************z_set.c**************************/

/* Delete an element with matching score/element from the skiplist.
 * The function returns 1 if the node was found and deleted, otherwise
 * 0 is returned.
 * 从跳表中删除一个分数/成员匹配的元素。如果找到这个节点并删除,这个函数会返回1,否则返回0。
 *
 * If 'node' is NULL the deleted node is freed by zslFreeNode(), otherwise
 * it is not freed (but just unlinked) and *node is set to the node pointer,
 * so that it is possible for the caller to reuse the node (including the
 * referenced SDS string at node->ele). 
 * 如果节点是NULL,那么删除的节点会被zslFreeNode()释放,否则只是解除链接,*node会被设置为节点指针,
 * 这样调用者可以重用节点(包括节点中引用的SDS字符串node->ele)。
 * 
 * */
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    int i;

    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        while (x->level[i].forward &&
                (x->level[i].forward->score < score ||
                    (x->level[i].forward->score == score &&
                     sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            x = x->level[i].forward;
        }
        update[i] = x;
    }
    /* We may have multiple elements with the same score, what we need
     * is to find the element with both the right score and object. 
     * 同一个score可能对应多个元素,我们需要找到score和对象都匹配的元素。
     */
    x = x->level[0].forward;
    if (x && score == x->score && sdscmp(x->ele,ele) == 0) {
        zslDeleteNode(zsl, x, update);
        if (!node)
            zslFreeNode(x);
        else
            *node = x;
        return 1;
    }
    return 0; /* not found */
}

其主要逻辑为:

  1. 遍历每一层,使用update[i]存储第i层最后一个小于score的节点。
  2. 遍历第0层,找到score和ele都匹配的节点。
  3. 调用zslDeleteNode删除节点。
  4. 释放节点。

zslDeleteNode-删除跳表中的节点 #

zslDeleteNode函数的源码如下:

/* Internal function used by zslDelete, zslDeleteRangeByScore and
 * zslDeleteRangeByRank.
 * 内部函数,会被zslDelete,zslDeleteRangeByScore和zslDeleteRangeByRank调用。
  */
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    int i;
    for (i = 0; i < zsl->level; i++) {
        if (update[i]->level[i].forward == x) {
            update[i]->level[i].span += x->level[i].span - 1;
            update[i]->level[i].forward = x->level[i].forward;
        } else {
            update[i]->level[i].span -= 1;
        }
    }
    if (x->level[0].forward) {
        x->level[0].forward->backward = x->backward;
    } else {
        zsl->tail = x->backward;
    }
    while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
        zsl->level--;
    zsl->length--;
}

其主要逻辑为:

  1. 遍历每一层,更新span和forward指针。即原来a->b->c,现在改为a->c,并将a的span-1。
  2. 如果要删除的节点x在最底层(0层)是尾节点,则将尾节点指向x的上一个节点。否则,将x的下一个节点的backward指针指向x的上一个节点。
  3. 如果跳表的头节点的最高层为空(跳表头节点的最高一层的forward前向指针指向NULL),则将跳表的层数减1。
  4. 将跳表的长度(元素个数)length 减1。