UNLINK vs DEL – An in-depth look at how this works inside Redis 🪢

A few days ago, I found myself debating the differences between Radishes‘ UNLINK
and DEL
order of my friend Significantly on a social media comment section. An interesting take; I find that most people seem to think so “DEL
is a blocking command. while UNLINK
is non-blocking – so UNLINK
better”. I do not fully agree with this attitude.
This is somewhat true – but it’s not the whole story. As an engineer, it is my moral duty not to dive down the rabbit hole and dig into Redis codebase to see the actual implementation. Let’s take a look at what’s really going on under the hood.
TLDR;
DEL vs UNLINK – the only difference is the way they release the value (the release of the key is straightforward). Respectfully, It is wrong to just say that one is blocking and the other is not.
UNLINK is a smart command: it’s not always non-blocking/async. It calculates the deallocation cost of an item, and if it’s too small (freeing cost < 64), it just does what DEL is supposed to do and frees the item ASAP. Otherwise, the item is sent to the background queue for processing.
What are UNLINK and DEL commands?
For my internet friends who are not familiar Radishes – this is a super popular, distributed in-memory key-value database. As the name suggests, both UNLINK
and DEL
The commands do the same thing – they retrieve the keys from Redis.
A quick Google search tells you why UNLINK introduced in Redis 4.0, when we already have DEL. UNLINK
very similar to DEL
but it does memory reclaiming in a different thread – so it does not block other operations, while DEL is. In simple terms, UNLINK just unlinks the key from the keyspace, and the actual removal of the key happens later, asynchronously – so it’s faster.
Fun fact – In redis 6.0, a new configuration
lazyfree-lazy-user-del
is introduced – so if it is set to true, your DEL command will run like UNLINK.
But how is it implemented internally?
DEL command – source code analysis
It doesn’t take a genius to find something really popular db.c
in the redis codebase, and go to delCommand
method – thanks for finding the symbol on Github.
void delCommand(client *c) {
delGenericCommand(c,server.lazyfree_lazy_user_del);
}
No wonder – It just proxies to delGenericCommand
method with the lazy flag sent as the value of lazyfree_lazy_user_del
redis server config. I’m sure it is unlinkCommand
they just set it to always true – neat.
/* This command implements DEL and UNLINK. */
void delGenericCommand(client *c, int lazy) {
int numdel = 0, j;
for (j = 1; j < c->argc; j++) {
// cleaning up already expired data
if (expireIfNeeded(c->db,c->argv(j),0) == KEY_DELETED)
continue;
// ternary operator to call method based on the lazy flag
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);
}
Too many to go through – but this beautiful ternary operator caught my attention – the lazy flag decides which method to call. Now, we go deeper into dbSyncDelete
and back to dbAsyncDelete
in UNLINK code analysis.
Maintain – Redis use
j
as variable name? Where are all those clean code evangelists who lectured me about meaningful variable names? just don’t.
Let’s go inside dbSyncDelete
. Let me guess – it’s not hard to remove a key from a hash table. Ah, not at all. My immature high-level language lover mind is useless garbage collectors. For a language like C, it is very interesting and important to pay attention to how memory is freed.
/* Delete a key, value and associated expiration entry if any, from the DB */
int dbSyncDelete(redisDb *db, robj *key) {
return dbGenericDelete(db, key, 0, DB_FLAG_KEY_DELETED);
}
Ah, it’s a proxy dbGenericDelete
with async
argument set as 0. Cool. Let’s go to dbGenericDelete
.
/* Helper for sync and async delete. */
int dbGenericDelete(redisDb *db, robj *key, int async, int flags) {
dictEntry **plink;
int table;
int slot = getKeySlot(key->ptr);
dictEntry *de = kvstoreDictTwoPhaseUnlinkFind(db->keys, slot, key->ptr, &plink, &table);
if (de) {
robj *val = dictGetVal(de);
/* remove key from histogram */
updateKeysizesHist(db, slot, val->type, getObjectLength(val), 0);
/* If hash object with expiry on fields, remove it from HFE DS of DB */
if (val->type == OBJ_HASH)
hashTypeRemoveFromExpires(&db->hexpires, val);
/* RM_StringDMA may call dbUnshareStringValue which may free val, so we
* need to incr to retain val */
incrRefCount(val);
/* Tells the module that the key has been unlinked from the database. */
moduleNotifyKeyUnlink(key,val,db->id,flags);
/* We want to try to unblock any module clients or clients using a blocking XREADGROUP */
signalDeletedKeyAsReady(db,key,val->type);
/* We should call decr before freeObjAsync. If not, the refcount may be
* greater than 1, so freeObjAsync doesn't work */
decrRefCount(val);
if (async) {
/* Because of dbUnshareStringValue, the val in de may change. */
freeObjAsync(key, dictGetVal(de), db->id);
kvstoreDictSetVal(db->keys, slot, de, NULL);
}
/* Deleting an entry from the expires dict will not free the sds of
* the key, because it is shared with the main dictionary. */
kvstoreDictDelete(db->expires, slot, key->ptr);
kvstoreDictTwoPhaseUnlinkFree(db->keys, slot, de, plink, table);
return 1;
} else {
return 0;
}
}
Ahh I found it overwhelming too!! But I found some very interesting things here.
1. Key slot calculation
The method int slot = getKeySlot(key->ptr);
calculate the slot for the given key. Redis uses the hash slot mechanism to distribute keys among the nodes in a cluster. Shamelessly plugging away my tweet which explains the mechanism in little detail.
2. Two-phase connection
Redis has moved away from the traditional way of removing the long back key. kvstoreDictTwoPhaseUnlinkFind
method finds the key in the main dictionary using the slot number and the key. This is the first phase – it does not remove the key instead of setting it plink
and table
. In the second phase – the key is safely removed. kvstoreDictTwoPhaseUnlinkFree
this is what I mean here. It then frees the memory.
3. Asynchronous deletion
the if (async)
The code block does a bunch of really cool things, like scheduling memory to be freed asynchronously. And also set up the value of the main dict entry to NULL to mark it for deletion. We will examine this in the UNLINK section.
4. Delete the expiration data
The interesting thing to note is redis maintains 2 dictionaries – the primary key dictionary and expiration dictionary. So, as a key is deleted – it must be removed from the expiration dict also – manually. This is the- kvstoreDictDelete
done in the method.
Well – what did you learn here? The normal implementation of the DEL command is straight forward. Simultaneously remove the key and release the memory using a two-phase linking mechanism. It is made of primary key dictionary and expiration dictionary. And if there is a content – recursive deletion comes to the rescue.
UNLINK command – source code analysis
As we have covered the core logic and the code is generic to async
flag passed, let’s dive straight into how async delete actually happens.
if (async) {
/* Because of dbUnshareStringValue, the val in de may change. */
freeObjAsync(key, dictGetVal(de), db->id);
kvstoreDictSetVal(db->keys, slot, de, NULL);
}
Wow, jump in freeObjAsync
.
void freeObjAsync(robj *key, robj *obj, int dbid) {
size_t free_effort = lazyfreeGetFreeEffort(key,obj,dbid);
/* Note that if the object is shared, to reclaim it now it is not
* possible. This rarely happens, however sometimes the implementation
* of parts of the Redis core may call incrRefCount() to protect
* objects, and then call dbDelete().
* LAZYFREE_THRESHOLD = 64
* */
if (free_effort > LAZYFREE_THRESHOLD && obj->refcount == 1) {
atomicIncr(lazyfree_objects,1);
bioCreateLazyFreeJob(lazyfreeFreeObject,1,obj);
} else {
decrRefCount(obj);
}
}
Damn, that’s smart – it is calculated the cost of removing the item. Once it gets how expensive it is (free_effort shows the efforts in terms of CPU, time and memory usage maybe?) – it decides either to release the memory immediately or delay it later on once it has enough CPU cycles to do so.
LAZYFREE_THRESHOLD
set at 64. And the check obj->refcount == 1
makes sure the object isn’t referenced anywhere else it might get stuck removing its references over and over again.
atomicIncr
just an atomic increment counter operation on a global variable lazyfree_objects
with the number of items currently frozen to be lazy. This helps redis to schedule background operations.
bioCreateLazyFreeJob
is responsible for doing background work IO (BIO) to lazily release the object. lazyfreeFreeObject
a callback function defined on line 13 of this file.
How is the key removal effort calculated?
The codebase of Redis says – the return value is not always the actual number of allocations that the object contains, but a number proportional to it.
- for strings, it’s always 1 as it doesn’t need many free allocations – so constant effort
- for list items – this is the number of elements in the quicklist.
- for set items – this is the number of elements in the hash table as the set is backed up by the hashtable.
- for ordered set items – this is the length of the skiplist
- for hash objects – this is the number of elements in the hash table
- for stream objects – this is the number of RAX nodes + groups of consumers + entries in the pending entry list (PEL)
- for the module – the process of calculating the value in this case seems a bit complicated for me to understand. It uses
moduleGetFreeEffort
. - default, it is always 1.
The magic number LAZYFREE_THRESHOLD = 64
However, there is no clear explanation of LAZYFREE_THRESHOLD
as 64, so it seems a bit arbitrary. A couple of my friends who are strangers to the internet and chatGPT say that this number was chosen by redis developers who posted a large benchmarking. The consideration is trade-off such as performance vs blocking, memory management and avoiding overhead.
Ok, I believe that.
Now that I have finished my ramblings, I invite your comments on this. If you find any technical inaccuracies, let me know, please. I am active on X (twitter) as @the2ndfloorguy and if you’re interested in what an unfunny and awesome programmer is up to next, see you there!
References, which I use.
https://pankajtanwar.in/static/images/twitter-card.png
2025-01-19 09:34:00