Redis源码阅读之hdel命令

Redis源码阅读之hdel命令

June 22, 2024
Redis
Redis
Redis版本:7.0.2

hdelCommand #

hdel命令对应的函数为hdelCommand,源码如下:

/*****************t_hash.c********************/
void hdelCommand(client *c) {
    robj *o;
    int j, deleted = 0, keyremoved = 0;

    if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL ||
        checkType(c,o,OBJ_HASH)) return;

    for (j = 2; j < c->argc; j++) {
        if (hashTypeDelete(o,c->argv[j]->ptr)) {
            deleted++;
            if (hashTypeLength(o) == 0) {
                dbDelete(c->db,c->argv[1]);
                keyremoved = 1;
                break;
            }
        }
    }
    if (deleted) {
        signalModifiedKey(c,c->db,c->argv[1]);
        notifyKeyspaceEvent(NOTIFY_HASH,"hdel",c->argv[1],c->db->id);
        if (keyremoved)
            notifyKeyspaceEvent(NOTIFY_GENERIC,"del",c->argv[1],
                                c->db->id);
        server.dirty += deleted;
    }
    addReplyLongLong(c,deleted);
}

其重要逻辑为:

  1. 通过lookupKeyWriteOrReply函数查找key对应的值对象,如果不存在则返回0。如果存在则检查值对象的类型是否为OBJ_HASH,如果不是则返回。
  2. 遍历参数列表,对每个参数调用hashTypeDelete函数删除哈希表中的元素。
  3. 如果删除元素之后,hash的元素个数为0,则调用dbDelete删除key(即在全局字典中删除key)。
  4. 如果删除了元素,则调用signalModifiedKey函数发送信号,调用notifyKeyspaceEvent函数发送事件通知,更新server.dirty。
  5. 最后调用addReplyLongLong函数返回删除的元素个数。

hashTypeDelete #

我们来看看hashTypeDelete函数是如何删除hash中的元素的:

/* Delete an element from a hash.
 * Return 1 on deleted and 0 on not found. */
int hashTypeDelete(robj *o, sds field) {
    int deleted = 0;

    if (o->encoding == OBJ_ENCODING_LISTPACK) {
        unsigned char *zl, *fptr;

        zl = o->ptr;
        fptr = lpFirst(zl);
        if (fptr != NULL) {
            fptr = lpFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);
            if (fptr != NULL) {
                /* Delete both of the key and the value. */
                zl = lpDeleteRangeWithEntry(zl,&fptr,2);
                o->ptr = zl;
                deleted = 1;
            }
        }
    } else if (o->encoding == OBJ_ENCODING_HT) {
        if (dictDelete((dict*)o->ptr, field) == C_OK) {
            deleted = 1;

            /* Always check if the dictionary needs a resize after a delete. */
            if (htNeedsResize(o->ptr)) dictResize(o->ptr);
        }

    } else {
        serverPanic("Unknown hash encoding");
    }
    return deleted;
}

其重要逻辑为:

  1. 判断类型,如果是listpack类型,则按照其规则进行删除。
  2. 如果是hashtable类型,则调用dictDelete函数删除元素。
  3. 如果是hashtable类型,删除之后调用htNeedsResize判断是否需要进行缩容,如果需要则调用dictResize函数进行缩容。
  4. 返回删除结果。

htNeedsResize-缩容的条件 #

那么,究竟在什么条件下会进行缩容呢?缩容时又会做什么呢?我们来看看htNeedsResize函数和dictResize函数:

/**************server.c****************/
int htNeedsResize(dict *dict)
{
    long long size, used;

    size = dictSlots(dict);
    used = dictSize(dict);
    return (size > DICT_HT_INITIAL_SIZE &&
            (used * 100 / size < HASHTABLE_MIN_FILL));
}

/*************dict.c***************/
/* Resize the table to the minimal size that contains all the elements,
 * but with the invariant of a USED/BUCKETS ratio near to <= 1 */
int dictResize(dict *d)
{
    unsigned long minimal;

    if (!dict_can_resize || dictIsRehashing(d))
        return DICT_ERR;
    minimal = d->ht_used[0];
    if (minimal < DICT_HT_INITIAL_SIZE)
        minimal = DICT_HT_INITIAL_SIZE;
    return dictExpand(d, minimal);
}

可以看到,要进行缩容,需同时满足下面两个条件:

  1. hash表的大小大于DICT_HT_INITIAL_SIZE(4)。
  2. hash表中元素个数*10,再除以hash表大小,小于HASHTABLE_MIN_FILL(10)。说的再直接一点,就是hash表的使用率小于10%,例如:hash表数组的长度为100,但是在整个hash表所拥有的元素个数(含链表中的)小于10个,那么就会进行缩容。

而缩容时,会将hash表的大小调整为当前hash表中元素个数,然后调用dictExpand函数进行缩容。

dictExpand又会调用_dictExpand,下面我们来看一下_dictExpand:

_dictExpand-缩容的方法 #

实际上_dictExpand不仅仅是用来进行缩容的,它还可以用来进行扩容或创建哈希表。下面我们来看一下_dictExpand的源码:

/************************dict.c****************************/

/* Expand or create the hash table,
 * when malloc_failed is non-NULL, it'll avoid panic if malloc fails (in which case it'll be set to 1).
 * Returns DICT_OK if expand was performed, and DICT_ERR if skipped. 
 * 扩容或创建hash表,
 * 当malloc_failed不为NULL时,如果malloc失败,将避免panic(此时将设置为1)。
 * 如果扩容成功返回DICT_OK,否则返回DICT_ERR。
 * 
 * 
 * */
int _dictExpand(dict *d, unsigned long size, int *malloc_failed)
{
    if (malloc_failed)
        *malloc_failed = 0;

    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    if (dictIsRehashing(d) || d->ht_used[0] > size)
        return DICT_ERR;

    /* the new hash table */
    dictEntry **new_ht_table;
    unsigned long new_ht_used;
    signed char new_ht_size_exp = _dictNextExp(size);

    /* Detect overflows */
    size_t newsize = 1ul << new_ht_size_exp;
    if (newsize < size || newsize * sizeof(dictEntry *) < newsize)
        return DICT_ERR;

    /* Rehashing to the same table size is not useful. */
    if (new_ht_size_exp == d->ht_size_exp[0])
        return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */
    if (malloc_failed)
    {
        new_ht_table = ztrycalloc(newsize * sizeof(dictEntry *));
        *malloc_failed = new_ht_table == NULL;
        if (*malloc_failed)
            return DICT_ERR;
    }
    else
        new_ht_table = zcalloc(newsize * sizeof(dictEntry *));

    new_ht_used = 0;

    /* Is this the first initialization? If so it's not really a rehashing
     * we just set the first hash table so that it can accept keys. */
    if (d->ht_table[0] == NULL)
    {
        d->ht_size_exp[0] = new_ht_size_exp;
        d->ht_used[0] = new_ht_used;
        d->ht_table[0] = new_ht_table;
        return DICT_OK;
    }

    /* Prepare a second hash table for incremental rehashing */
    d->ht_size_exp[1] = new_ht_size_exp;
    d->ht_used[1] = new_ht_used;
    d->ht_table[1] = new_ht_table;
    d->rehashidx = 0;
    return DICT_OK;
}

其主要逻辑为:

  1. 如果正在进行rehash,则返回错误。
  2. 生成新的hash表的容量值newsize,如果newsize值太小,则返回错误。
  3. 如果newsize值和现在哈希表大小相等,即等量库容,则返回错误。
  4. 尝试分配内存,如果分配失败,则返回错误。
  5. 如果当前哈希表ht_table[0]中为NULL,则直接将新的hash表赋值给旧的hash表。
  6. 否则,将新分配的hash表赋值值ht_table[1],将rehashidx置为0,标识正在进行rehash。

注意:当没有正在进行rehashshi,rehashidx为-1。

关于渐进式rehash的详细过程,我们单独再写一篇。