前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入理解redis的一个del和unlink的命令的执行过程-1

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

作者头像
公众号-利志分享
发布2022-04-25 09:25:33
2.1K0
发布2022-04-25 09:25:33
举报
文章被收录于专栏:利志分享

很多同学都用过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删除之后的内存回收使用的引用计算器算法。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 利志分享 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档