An In-Depth Analysis of Redis Multi-Threaded Architecture and I/O Event Processing
This article provides a comprehensive source-level analysis of Redis 6.0 and later multi-threaded architecture, detailing how the main event loop coordinates with dedicated I/O threads to efficiently distribute, parse, and process concurrent read and write requests while maintaining high throughput and low latency.
Redis is renowned for its high-performance single-threaded event loop using epoll, but this design limits multi-core utilization and causes request blocking during long operations. To address this, Redis 6.0 introduced a multi-threaded I/O model that offloads network read and write operations to worker threads while keeping command execution single-threaded.
During server initialization, the main thread sets up read and write task queues, creates an epoll instance, binds to the listening port, and registers the accept handler. Subsequently, the InitServerLast function spawns multiple I/O threads via pthread_create, each running an IOThreadMain loop that waits for tasks assigned by the main thread.
void InitServerLast() {
initThreadedIO();
...
}
void initThreadedIO(void) {
if (server.io_threads_num == 1) return;
for (int i = 0; i < server.io_threads_num; i++) {
pthread_t tid;
pthread_create(&tid,NULL,IOThreadMain,(void*)(long)i)
io_threads[i] = tid;
}
}The core event loop runs continuously in aeMain, invoking aeProcessEvents to poll for socket events using epoll_wait. When new connections arrive, they are accepted and registered with a read handler. In the multi-threaded mode, incoming read requests are immediately queued into server.clients_pending_read rather than processed synchronously.
Before the next epoll_wait cycle, the beforeSleep callback distributes pending read tasks across I/O threads using a round-robin hash. Both the main thread and worker threads execute readQueryFromClient to parse commands and populate response buffers. After processing, clients with pending replies are added to server.clients_pending_write.
int handleClientsWithPendingReadsUsingThreads(void) {
listRewind(server.clients_pending_read,&li);
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++;
}
io_threads_op = IO_THREADS_OP_READ;
for (int j = 1; j < server.io_threads_num; j++) {
int count = listLength(io_threads_list[j]);
setIOPendingCount(j, count);
}
listRewind(io_threads_list[0],&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
readQueryFromClient(c->conn);
}
while(1) {
unsigned long pending = 0;
for (int j = 1; j < server.io_threads_num; j++)
pending += getIOPendingCount(j);
if (pending == 0) break;
}
...
}The write phase follows a similar parallel distribution pattern. The handleClientsWithPendingWritesUsingThreads function assigns clients to I/O threads, which then invoke writeToClient to flush buffered responses to sockets. Once all I/O operations complete, the main thread resumes polling for new events.
While this architecture improves network I/O throughput, the author notes a limitation: the main thread still synchronously waits for all I/O threads to finish before proceeding. Consequently, a single slow command can still block the event loop, indicating that the multi-threading model primarily optimizes I/O rather than fully parallelizing command execution.
Refining Core Development Skills
Fei has over 10 years of development experience at Tencent and Sogou. Through this account, he shares his deep insights on performance.
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.