Databases 16 min read

Redis Lazy Free and Multi‑Threaded I/O: Architecture, Mechanisms, and Limitations

This article explains how Redis, originally a single‑threaded in‑memory cache, introduced Lazy Free in version 4.0 and multi‑threaded I/O in version 6.0 to mitigate blocking deletions and improve I/O throughput, detailing the underlying event model, code implementations, performance trade‑offs, and comparisons with Tair's threading design.

Top Architect
Top Architect
Top Architect
Redis Lazy Free and Multi‑Threaded I/O: Architecture, Mechanisms, and Limitations

Redis, a memory‑based caching system, is known for its high performance due to a single‑threaded design that avoids context switches and locking, achieving read speeds of up to 110 k ops/s and write speeds of 81 k ops/s. However, this design limits Redis to a single CPU core, can block for seconds when deleting large keys (e.g., a Set with millions of elements), and restricts further QPS improvements.

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) and time events (periodic tasks like key expiration and server statistics). It abstracts these events using a Reactor pattern and processes them in a single thread, which simplifies implementation and avoids lock contention.

Because the event loop processes file events before time events, all network I/O and command handling occur in a single thread, while occasional background processes (e.g., RDB snapshot generation) are forked into separate processes.

Lazy Free Mechanism

To prevent long pauses caused by slow commands like deleting a massive Set or executing FLUSHALL , Redis 4.0 introduced Lazy Free , which offloads expensive deletions to a background thread. The UNLINK command replaces the synchronous DEL by first unlinking the key and then freeing its memory asynchronously.

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);
}

The asynchronous path calculates a "free effort" value; if it exceeds a threshold (e.g., 64 allocations) and the object is not shared, the key/value pair is placed on a background job queue for lazy freeing.

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 added a dedicated I/O Thread pool to handle network reads and writes in parallel, while the original event‑handling thread continues to process commands. The event thread distributes pending client reads among the I/O threads, which then perform the actual socket operations.

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 the other threads to end their work.
    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 thread main loop reads or writes data for its assigned clients and reports completion back to the event thread.

void *IOThreadMain(void *myid) {
    while (1) {
        // I/O thread executes read/write operations
        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\n", id);
    }
}

Despite the parallel I/O, the design is not a full pipeline: I/O threads can only read or write at a time, and the event‑handling thread remains idle while I/O threads work, limiting scalability.

Comparison with Tair's Threading Model

Tair separates responsibilities into a Main Thread (client connection), an I/O Thread (read, parse, write), and a Worker Thread (command execution). Lock‑free queues and pipes exchange data between I/O and Worker threads, achieving higher parallelism and better performance than Redis's native approach.

Conclusion

Redis introduced Lazy Free in 4.0 to solve blocking deletions and added multi‑threaded I/O in 6.0, which yields modest performance gains (about 2× over single‑threaded). Compared with Tair, Redis's threading is less elegant and offers limited speedup. Future improvements are expected to focus on slow‑operation threading and module‑level key locking rather than full I/O threading.

Memory ManagementRedisDatabase PerformanceMultithreaded I/OLazy Free
Top Architect
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.