How Redis Overcame Single‑Thread Limits with Lazy Free and Multi‑Threaded I/O
This article explains how Redis, originally a single‑threaded in‑memory cache, introduced Lazy Free and multi‑threaded I/O to eliminate blocking during large key deletions, improve QPS, and lay the groundwork for future multithreading strategies.
Redis Single‑Threaded Architecture
Redis is an in‑memory cache known for high performance, achieving read speeds of 110,000 ops/s and write speeds of 81,000 ops/s thanks to its lock‑free, single‑threaded design. However, this design has drawbacks: it can only use one CPU core, large key deletions (e.g., a Set with millions of members) can block the server for seconds, and overall QPS cannot be increased.
Only one CPU core is utilized.
Deleting massive keys may block the server for several seconds.
QPS growth is limited.
To address these issues, Redis 4.0 introduced Lazy Free and Redis 6.0 added Multi‑threaded I/O, gradually moving toward a multithreaded model.
Event‑Driven Core
Redis operates as an event‑driven server handling two kinds of events: File events: abstract socket operations such as accept, read, write, and close. Time events: scheduled tasks like key expiration and server statistics.
The server uses a Reactor pattern with I/O multiplexing, processing file events first and then time events, all within a single thread.
Lazy Free Mechanism
When a client issues a slow command such as deleting a massive Set or executing FLUSHALL, the server would block. Redis 4.0 introduced Lazy Free to make these slow operations asynchronous. The UNLINK command delegates memory reclamation to a background thread, allowing the main thread to quickly unlink the key and continue processing other events.
Key deletion flow (simplified):
void delCommand(client *c) {
delGenericCommand(c, server.lazyfree_lazy_user_del);
}
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 evaluates the "free effort" of an object; if it exceeds a threshold (e.g., 64 allocations) and the object is not shared, it is placed on a background job queue for lazy freeing.
#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 in Redis 6.0
Redis 6.0 introduced a dedicated I/O Thread pool to handle read/write operations, while the original event thread continues processing commands. The event thread distributes ready read events to I/O threads, waits for them to finish, then processes the commands and finally hands write events back to the I/O threads.
Client distribution example:
int handleClientsWithPendingReadsUsingThreads(void) {
// Distribute the clients across N different lists.
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;
}I/O thread main loop:
void *IOThreadMain(void *myid) {
while (1) {
// Process assigned clients.
while ((ln = listNext(&li))) {
client *c = listNodeValue(ln);
if (io_threads_op == IO_THREADS_OP_WRITE) {
writeToClient(c, 0);
} else if (io_threads_op == IO_THREADS_OP_READ) {
readQueryFromClient(c->conn);
} else {
serverPanic("io_threads_op value is unknown");
}
}
listEmpty(io_threads_list[id]);
io_threads_pending[id] = 0;
if (tio_debug) printf("[%ld] Done
", id);
}
}Limitations: I/O threads can only perform reads or writes at a time, and the event thread remains idle while waiting, so the design is not a full pipeline and incurs polling overhead.
Comparison with Tair
Tair implements a more elegant multithreading model with a main thread for connections, I/O threads for parsing, and worker threads for command execution, communicating via lock‑free queues and pipes, achieving higher performance gains.
Conclusion
Redis 4.0 added the Lazy Free thread to solve blocking caused by large‑key deletions, and Redis 6.0 introduced I/O Thread to provide multithreaded I/O. Compared with Tair, Redis's multithreading is less elegant and yields modest performance gains (about 2×). Future directions may focus on slow‑operation threading and improving cluster scalability rather than full I/O threading.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Interview Crash Guide
Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.
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.
