Redis Architecture: From Single‑Threaded Core to Multi‑Threaded I/O and BIO Evolution
This article explains how Redis evolved from a single‑threaded event‑loop architecture using I/O multiplexing and the Reactor pattern to a multi‑threaded I/O model and an enhanced BIO system with lazyfree, detailing design decisions, source‑code excerpts, performance impacts, and practical lessons for developers.
Redis historically used a single‑threaded Reactor model where a single event loop handled all client connections using I/O multiplexing (epoll, kqueue, select). The core network stack processed commands in the main thread, while background BIO threads handled asynchronous tasks such as AOF persistence and file closing.
The article describes the fundamental components of this design, including the client structure, the aeApiPoll function, and the event‑loop handlers ( acceptTcpHandler , readQueryFromClient , sendReplyToClient ). It shows how Redis encapsulates I/O multiplexing APIs and selects the most efficient implementation (evport, epoll, kqueue, select) based on the platform.
Starting with Redis 6.0, a multi‑threaded I/O model was introduced. The main thread still accepts new connections, but read and write operations are off‑loaded to a pool of I/O threads using a round‑robin scheduler. The workflow is illustrated with pseudo‑code: void *IOThreadMain(void *myid) { while (1) { if (io_threads_pending[id] == 0) { pthread_mutex_lock(&io_threads_mutex[id]); pthread_mutex_unlock(&io_threads_mutex[id]); continue; } // process assigned clients if (io_threads_op == IO_THREADS_OP_READ) readQueryFromClient(c->conn); else if (io_threads_op == IO_THREADS_OP_WRITE) writeToClient(c,0); io_threads_pending[id] = 0; } } This design keeps command execution in the main thread while parallelizing network I/O, improving QPS by nearly 2× when many I/O threads are configured.
The BIO subsystem, present since early Redis versions, uses a producer‑consumer model with condition variables and mutexes to perform background file operations. The article walks through initialization, job creation, and processing: void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3) { struct bio_job *job = zmalloc(sizeof(*job)); pthread_mutex_lock(&bio_mutex[type]); listAddNodeTail(bio_jobs[type], job); pthread_cond_signal(&bio_condvar[type]); pthread_mutex_unlock(&bio_mutex[type]); } In Redis 4.0, a new lazyfree member was added to the BIO system, allowing large keys to be deleted asynchronously. The unlinkCommand triggers dbAsyncDelete , which off‑loads objects exceeding a threshold to a background thread, avoiding main‑thread blocking.
Performance benchmarks from external sources demonstrate that enabling 24 I/O threads can raise GET/SET throughput to over 200 K QPS, roughly doubling the single‑threaded performance. The article concludes with a summary of all Redis threads (main server, BIO threads, lazyfree thread, and I/O workers) and emphasizes the architectural lessons for building high‑performance, scalable systems.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.