Redis源码阅读之hdel命令
June 22, 2024
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);
}
其重要逻辑为:
- 通过lookupKeyWriteOrReply函数查找key对应的值对象,如果不存在则返回0。如果存在则检查值对象的类型是否为OBJ_HASH,如果不是则返回。
- 遍历参数列表,对每个参数调用hashTypeDelete函数删除哈希表中的元素。
- 如果删除元素之后,hash的元素个数为0,则调用dbDelete删除key(即在全局字典中删除key)。
- 如果删除了元素,则调用signalModifiedKey函数发送信号,调用notifyKeyspaceEvent函数发送事件通知,更新server.dirty。
- 最后调用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;
}
其重要逻辑为:
- 判断类型,如果是listpack类型,则按照其规则进行删除。
- 如果是hashtable类型,则调用dictDelete函数删除元素。
- 如果是hashtable类型,删除之后调用htNeedsResize判断是否需要进行缩容,如果需要则调用dictResize函数进行缩容。
- 返回删除结果。
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);
}
可以看到,要进行缩容,需同时满足下面两个条件:
- hash表的大小大于DICT_HT_INITIAL_SIZE(4)。
- 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;
}
其主要逻辑为:
- 如果正在进行rehash,则返回错误。
- 生成新的hash表的容量值newsize,如果newsize值太小,则返回错误。
- 如果newsize值和现在哈希表大小相等,即等量库容,则返回错误。
- 尝试分配内存,如果分配失败,则返回错误。
- 如果当前哈希表ht_table[0]中为NULL,则直接将新的hash表赋值给旧的hash表。
- 否则,将新分配的hash表赋值值ht_table[1],将rehashidx置为0,标识正在进行rehash。
注意:当没有正在进行rehashshi,rehashidx为-1。
关于渐进式rehash的详细过程,我们单独再写一篇。