深入理解redis的一个del和unlink的命令的执行过程-1

2022-04-25 09:25:33 浏览数 (1)

很多同学都用过redis的del,但是unlink这个命令相对来说应该比较陌生一些,del在redis刚开始的第一个版本1.0.0就有了,unlink则不是,unlink是从4.0.0开始有的这个命令。两个命令都是一样的功能,表示删除key。但是它们有什么区别呢?使用需要注意什么问题呢?下面通过了解源码来讲解(redis版本源码是4.0.13)。

从redis/src/server.c中我们找到del和unlink命令的执行命令。

代码语言:javascript复制
// del这里会调用delCommand方法,是一个普通的写
{"del",delCommand,-2,
 "write @keyspace",
 0,NULL,1,-1,1,0,0,0},


// unlink 这里会调用unlinkCommand方法,write fast,表示是一个快速的写
{"unlink",unlinkCommand,-2,
 "write fast @keyspace",
 0,NULL,1,-1,1,0,0,0},

这里是定义的命令,分别到redis/src/db.c里面进行执行。这里发现del和unlink是调用的同一个方法delGenericCommand,只是传入的参数不一样,del传入的第二个参数是0,unlink则是1。

代码语言:javascript复制
void delCommand(client *c) {
    delGenericCommand(c,server.lazyfree_lazy_user_del);
}

void unlinkCommand(client *c) {
    delGenericCommand(c,1);
}

下面我们来看下redis/src/db.c里面的delGenericCommand方法。

代码语言:javascript复制
void delGenericCommand(client *c, int lazy) {
    int numdel = 0, j;

    for (j = 1; j < c->argc; j  ) {
        // 自动过期数据清理
        expireIfNeeded(c->db,c->argv[j]);
        // 此处分同步删除和异步删除, 主要差别在于对于复杂数据类型的删除方面,如hash,list,set...当然,这个结论是通过后面的源码分析出来的,这里先写一下结果。针对 string 的删除是完全一样的
        int deleted  = lazy ? dbAsyncDelete(c->db,c->argv[j]) :
                              dbSyncDelete(c->db,c->argv[j]);
        // 命令执行结果记录下来
        if (deleted) {
            signalModifiedKey(c,c->db,c->argv[j]);
            notifyKeyspaceEvent(NOTIFY_GENERIC,
                "del",c->argv[j],c->db->id);
            server.dirty  ;
            numdel  ;
        }
    }
    // 响应删除数据量
    addReplyLongLong(c,numdel);
}

del命令执行的是dbSyncDelete方法,unlink命令执行的是dbAsyncDelete方法。

下面我们先看dbSyncDelete删除,方法在redis/src/db.c里面。

代码语言:javascript复制
int dbSyncDelete(redisDb *db, robj *key) {
    /* Deleting an entry from the expires dict will not free the sds of
     * the key, because it is shared with the main dictionary. */
    // 首先从 expires 队列删除,然后再从 db->dict 中删除
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
    dictEntry *de = dictUnlink(db->dict,key->ptr);
    if (de) {
        robj *val = dictGetVal(de);
        /* Tells the module that the key has been unlinked from the database. */
        moduleNotifyKeyUnlink(key,val);
        dictFreeUnlinkedEntry(db->dict,de);
        if (server.cluster_enabled) slotToKeyDel(key->ptr);
        return 1;
    } else {
        return 0;
    }
}

上面我们看到会执行dictDelete,此方法在redis/src/dict.c里面

代码语言:javascript复制
/* 删除方法,成功返回DICK_OK,否则返回DICK_ERR */
int dictDelete(dict *ht, const void *key) {
    // nofree: 0, 即要求释放内存
    return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}

/* 查找并删除一个元素,是dictDelete()和dictUnlink()的辅助函数。*/
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
    uint64_t h, idx;
    dictEntry *he, *prevHe;
    int table;

    if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;

    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);


    // 这里也是需要考虑到rehash的情况,ht[0]和ht[1]中的数据都要删除掉 
    for (table = 0; table <= 1; table  ) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        prevHe = NULL;
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                /* 从列表中unlink掉元素 */
                if (prevHe)
                    prevHe->next = he->next;
                else
                    d->ht[table].table[idx] = he->next;
                // 如果nofree是0,需要释放k和v对应的内存空间 
                if (!nofree) {
                    dictFreeKey(d, he);
                    dictFreeVal(d, he);
                    zfree(he);
                }
                d->ht[table].used--;
                return he;
            }
            prevHe = he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return NULL; /* 没找到key对应的数据 */
}

对于有GC收集器的语言来说,根本不用关注内存的释放问题,自有后台工具处理,然而对于 c 语言这种级别语言,则是需要自行关注内存的。所以从上面看数据已经删除了,会涉及到回收。我们来看下回收的代码。

代码语言:javascript复制
// redis/src/dict.h, 释放key, value,释放依赖于 keyDestructor, valDestructor
#define dictFreeKey(d, entry) 
    if ((d)->type->keyDestructor) 
        (d)->type->keyDestructor((d)->privdata, (entry)->key)
#define dictFreeVal(d, entry) 
    if ((d)->type->valDestructor) 
        (d)->type->valDestructor((d)->privdata, (entry)->v.val)

/* 
所以,我们有必要回去看看 key,value 的析构方法
而这,又依赖于具体的数据类型,也就是你在 setXXX 的时候用到的数据类型
我们看一下这个 keyDestructor,valDestructor 初始化的样子
redis/src/server.c  kv的析构函数定义
*/
/* Db->dict, keys are sds strings, vals are Redis objects. */
dictType dbDictType = {
    dictSdsHash,                /* hash function */
    NULL,                       /* key dup */
    NULL,                       /* val dup */
    dictSdsKeyCompare,          /* key compare */
    dictSdsDestructor,          /* key destructor */
    dictObjectDestructor,       /* val destructor */
    dictExpandAllowed           /* allow to expand */
};

// key destructor, key 的释放,直接调用 sds 提供的服务即可,redis/src/server.c
void dictSdsDestructor(void *privdata, void *val)
{
    DICT_NOTUSED(privdata);
    // sds 直接释放key就行了
    sdsfree(val);
}

/* 释放sds的占用的空间,redis/src/sds.c */
void sdsfree(sds s) {
    if (s == NULL) return;
    // zfree, 确实很简单, 因为 sds 是连续的内存空间,直接使用系统提供的方法即可删除
    s_free((char*)s-sdsHdrSize(s[-1]));
}

// 关于value的释放,如果说 key 一定是string格式的话,value可不一定了,因为 redis提供丰富的数据类型,下面的方式是redis/src/server.c
void dictObjectDestructor(void *privdata, void *val)
{
    DICT_NOTUSED(privdata);
    if (val == NULL) return; /* Lazy freeing will set value to NULL. */
    decrRefCount(val);
}

/* 减少引用计数,如果没有引用了就释放内存空间 在redis/src/object.c文件中 */
void decrRefCount(robj *o) {
    if (o->refcount == 1) {
        switch(o->type) {
        // string 类型
        case OBJ_STRING: freeStringObject(o); break;
        // list 类型
        case OBJ_LIST:
        freeListObject(o); break;
        // set 类型
        case OBJ_SET: freeSetObject(o); break;
        // zset 类型
        case OBJ_ZSET: freeZsetObject(o); break;
        // hash 类型
        case OBJ_HASH: freeHashObject(o); break;
        case OBJ_MODULE: freeModuleObject(o); break;
        case OBJ_STREAM: freeStreamObject(o); break;
        default: serverPanic("Unknown object type"); break;
        }
        zfree(o);
    } else {
        if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");
        if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount--;
    }
}

// 下面是释放的方法,也是在redis/src/objec.c文件里面。
void freeStringObject(robj *o) {
    if (o->encoding == OBJ_ENCODING_RAW) {
        sdsfree(o->ptr);
    }
}
// redis/src/objec.c文件里面。
void freeListObject(robj *o) {
    if (o->encoding == OBJ_ENCODING_QUICKLIST) {
        quicklistRelease(o->ptr);
    } else {
        serverPanic("Unknown list encoding type");
    }
}
// redis/src/objec.c文件里面。
void freeSetObject(robj *o) {
    switch (o->encoding) {
    case OBJ_ENCODING_HT:
        dictRelease((dict*) o->ptr);
        break;
    case OBJ_ENCODING_INTSET:
        zfree(o->ptr);
        break;
    default:
        serverPanic("Unknown set encoding type");
    }
}
// redis/src/objec.c文件里面。
void freeZsetObject(robj *o) {
    zset *zs;
    switch (o->encoding) {
    case OBJ_ENCODING_SKIPLIST:
        zs = o->ptr;
        dictRelease(zs->dict);
        zslFree(zs->zsl);
        zfree(zs);
        break;
    case OBJ_ENCODING_ZIPLIST:
        zfree(o->ptr);
        break;
    default:
        serverPanic("Unknown sorted set encoding");
    }
}
// redis/src/objec.c文件里面。
void freeHashObject(robj *o) {
    switch (o->encoding) {
    case OBJ_ENCODING_HT:
        dictRelease((dict*) o->ptr);
        break;
    case OBJ_ENCODING_ZIPLIST:
        zfree(o->ptr);
        break;
    default:
        serverPanic("Unknown hash encoding type");
        break;
    }
}

/* 在redis/src/dict.c Clear & Release the hash table */
void dictRelease(dict *d)
{
    // ht[0],ht[1] 依次清理
    _dictClear(d,&d->ht[0],NULL);
    _dictClear(d,&d->ht[1],NULL);
    zfree(d);
}

/* 在redis/src/dict.c 清理到整个dict */
int _dictClear(dict *d, dictht *ht, void(callback)(void *)) {
    unsigned long i;

    /* Free all the elements */
    for (i = 0; i < ht->size && ht->used > 0; i  ) {
        dictEntry *he, *nextHe;

        if (callback && (i & 65535) == 0) callback(d->privdata);
        // 元素为空,hash未命中,但只要 used > 0, 代表就还有需要删除的元素存在
        if ((he = ht->table[i]) == NULL) continue;
        while(he) {
            nextHe = he->next;
            // 这里的释放 kv 逻辑和前面是一致的,看起来像是递归,其实不然,因为redis不存在数据类型嵌套问题,比如 hash下存储hash, 所以不会存在递归
            dictFreeKey(d, he);
            dictFreeVal(d, he);
            zfree(he);
            ht->used--;
            he = nextHe;
        }
    }
    /* Free the table and the allocated cache structure */
    zfree(ht->table);
    /* Re-initialize the table */
    _dictReset(ht);
    return DICT_OK; /* never fails */
}

好了上面看了redis的同步删除key的整个执行流程,已经差不多了。下面我们总结一下。

总结:

1:del和unlink的最大区别是del是同步删除,unlink是异步删除(目前异步删除的还没有讲解,下一篇文件讲)

2:对于线上使用删除的尽量不要使用del,因为同步删除可能会造成本身服务停顿,特别是业务量特别依赖redis的服务。

3:redis的value删除之后的内存回收使用的引用计算器算法。

0 人点赞