Redis Single‑Threaded Architecture, Lazy Free Mechanism, and Multi‑Threaded I/O
This article explains Redis's original single‑threaded event‑driven design, the problems it causes, how the Lazy Free mechanism and asynchronous deletion were introduced in Redis 4.0, and how Redis 6.0 adds multi‑threaded I/O while discussing its limitations and comparing it with Tair's approach.
Hello, I am a senior architect.
Redis, as an in‑memory cache, achieves high performance through a single‑threaded, lock‑free design, delivering read speeds of up to 110 k ops/s and write speeds of 81 k ops/s. However, this design has drawbacks: it can only use one CPU core, large key deletions can block the server for seconds, and QPS cannot be increased further.
Single‑Threaded Principle
Redis operates as an event‑driven server handling two kinds of events:
File events : socket operations such as accept , read , write , close generated by client connections.
Time events : scheduled tasks like key expiration and server statistics.
Both event types are processed in a single thread using the Reactor pattern and I/O multiplexing, allowing many clients to be served concurrently without locks.
Lazy Free Mechanism
When a client issues a time‑consuming command (e.g., deleting a Set with millions of members or executing FLUSHALL ), the single thread would block for seconds. Redis 4.0 introduced Lazy Free to make such slow operations asynchronous. The UNLINK command, for example, detaches the key and lets a background thread reclaim memory.
void delCommand(client *c) {
delGenericCommand(c, server.lazyfree_lazy_user_del);
}
/* This command implements DEL and LAZYDEL. */
void delGenericCommand(client *c, int lazy) {
int numdel = 0, j;
for (j = 1; j < c->argc; j++) {
expireIfNeeded(c->db, c->argv[j]);
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);
}Asynchronous deletion works by calculating a "free‑effort" value; if it exceeds a threshold, the object is placed into a background job queue instead of being freed synchronously.
#define LAZYFREE_THRESHOLD 64
int dbAsyncDelete(redisDb *db, robj *key) {
if (dictSize(db->expires) > 0) dictDelete(db->expires, key->ptr);
dictEntry *de = dictUnlink(db->dict, key->ptr);
if (de) {
robj *val = dictGetVal(de);
size_t free_effort = lazyfreeGetFreeEffort(val);
if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
atomicIncr(lazyfree_objects, 1);
bioCreateBackgroundJob(BIO_LAZY_FREE, val, NULL, NULL);
dictSetVal(db->dict, de, NULL);
}
}
if (de) {
dictFreeUnlinkedEntry(db->dict, de);
if (server.cluster_enabled) slotToKeyDel(key->ptr);
return 1;
} else {
return 0;
}
}Multi‑Threaded I/O and Its Limitations
Redis 6.0 adds a dedicated I/O thread pool to handle read/write operations, while the original event‑handling thread continues processing other events. Clients with pending reads are distributed across the I/O threads, which perform the actual socket reads/writes and then signal completion.
int handleClientsWithPendingReadsUsingThreads(void) {
// Distribute clients across N I/O threads
listIter li;
listNode *ln;
listRewind(server.clients_pending_read, &li);
int item_id = 0;
while ((ln = listNext(&li))) {
client *c = listNodeValue(ln);
int target_id = item_id % server.io_threads_num;
listAddNodeTail(io_threads_list[target_id], c);
item_id++;
}
// Wait for all I/O threads to finish
while (1) {
unsigned long pending = 0;
for (int j = 1; j < server.io_threads_num; j++)
pending += io_threads_pending[j];
if (pending == 0) break;
}
return processed;
}The I/O threads can only process reads or writes at a time, and the event‑handling thread remains idle while I/O threads work, which introduces polling overhead and prevents a true pipeline model.
Limitations
The multi‑threaded design does not fully parallelize event handling; the I/O threads are blocked by the single event‑handling thread, and lock‑free benefits are limited. The author notes that many deployments are network‑ or memory‑bound, so the performance gain is modest (about 2×).
Tair’s Multi‑Threaded Implementation
Tair separates responsibilities into a Main Thread (client connections), an I/O Thread (reading, parsing, writing), and Worker Threads (command execution). Communication uses lock‑free queues and pipes, allowing higher parallelism and better performance (about 3× over single‑threaded Redis).
Conclusion
Redis 4.0 introduced the Lazy Free thread to avoid blocking during large key deletions, and Redis 6.0 added I/O threads for limited multi‑threaded I/O. While these improvements mitigate some bottlenecks, the author believes true scalability will come from clustering and module‑based slow‑operation threading rather than full I/O threading.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.