Software & Apps

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

A few days ago, I found myself debating the differences between RadishesUNLINK 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.

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 unlinkCommandthey 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.

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

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button