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