How Redis Achieves High‑Performance Networking with a Single‑Threaded Event Loop
This article explains how Redis uses Linux epoll and a single‑threaded event loop to handle millions of connections efficiently, covering the creation of the epoll object, server initialization, event registration, the main processing loop, and the mechanisms for reading, writing, and managing pending tasks.
Understanding I/O Multiplexing
Before diving into Redis, the article introduces epoll , the Linux kernel mechanism that allows a single thread to monitor many sockets simultaneously, eliminating the need for one thread per connection.
Redis Server Startup Initialization
Redis’s entry point is int main(int argc, char **argv) in src/server.c. The main function calls initServer() to set up the event loop and then aeMain(server.el) to start the endless event‑processing loop.
# git clone https://github.com/redis/redis
# cd redis
# git checkout -b 5.0.0 5.0.0The core of initServer() performs three essential tasks:
Create an epoll object.
Bind the configured listening port.
Register the accept handler for the listening socket.
1. Creating the epoll Object
The function aeCreateEventLoop(int setsize) allocates an aeEventLoop structure and calls aeApiCreate(eventLoop), which in turn invokes epoll_create(1024) to obtain the kernel epoll file descriptor.
//file: src/ae.c
aeEventLoop *aeCreateEventLoop(int setsize) {
aeEventLoop *eventLoop;
eventLoop = zmalloc(sizeof(*eventLoop));
eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
aeApiCreate(eventLoop);
return eventLoop;
}
//file: src/ae_epoll.c
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));
state->epfd = epoll_create(1024);
eventLoop->apidata = state;
return 0;
}2. Binding the Listening Port
Redis calls
listenToPort(server.port, server.ipfd, &server.ipfd_count), which loops over the configured bind addresses and invokes anetTcpServer (a thin wrapper around socket, bind, and listen) to create the listening socket.
//file: src/redis.c
int listenToPort(int port, int *fds, int *count) {
for (j = 0; j < server.bindaddr_count || j == 0; j++) {
fds[*count] = anetTcpServer(server.neterr, port, NULL, server.tcp_backlog);
}
}
//file: src/anet.c
int anetTcpServer(char *err, int port, char *bindaddr, int backlog) {
return _anetTcpServer(err, port, bindaddr, AF_INET, backlog);
}
static int _anetTcpServer(...) {
// set SO_REUSEADDR, bind, listen ...
anetSetReuseAddr(err, s);
bind(s, sa, len);
listen(s, backlog);
}3. Registering the Accept Handler
For each listening socket, aeCreateFileEvent registers acceptTcpHandler as the readable callback.
//file: src/server.c
void initServer() {
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
listenToPort(server.port, server.ipfd, &server.ipfd_count);
for (j = 0; j < server.ipfd_count; j++) {
aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE, acceptTcpHandler, NULL);
}
}Redis Event‑Processing Loop
The infinite loop in aeMain repeatedly:
Calls beforesleep to flush pending write buffers.
Invokes aeProcessEvents, which internally calls epoll_wait to discover readable or writable events.
//file: src/ae.c
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}1. epoll_wait discovers events
When epoll_wait reports a ready socket, the loop retrieves the corresponding aeFileEvent from eventLoop->events and invokes the stored read or write callbacks.
2. Handling New Connections
The readable callback for the listening socket is acceptTcpHandler. It performs:
Accept the new TCP connection.
Create a redisClient object.
Register a readable event for the new client socket.
//file: src/networking.c
void acceptTcpHandler(aeEventLoop *el, int fd, ...) {
int cfd = anetTcpAccept(server.neterr, fd, ...);
acceptCommonHandler(cfd, 0);
}
static void acceptCommonHandler(int fd, int flags) {
redisClient *c = createClient(fd);
// further initialization ...
}3. Processing Client Commands
When a client socket becomes readable, readQueryFromClient is invoked. It reads the request, parses the command, executes it via call, and queues the reply.
//file: src/networking.c
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, ...) {
redisClient *c = (redisClient*)privdata;
processInputBufferAndReplicate(c);
}
void processInputBufferAndReplicate(client *c) {
processInputBuffer(c);
}
void processInputBuffer(redisClient *c) {
processCommand(c);
}The processCommand function looks up the command in redisCommandTable and either queues it (for MULTI) or calls the command implementation directly.
//file: src/server.c
int processCommand(redisClient *c) {
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
if (c->flags & CLIENT_MULTI) {
queueMultiCommand(c);
} else {
call(c, CMD_CALL_FULL);
}
return C_OK;
}For a GET request, getCommand eventually calls addReplyBulk to place the value into the client’s output buffer.
//file: src/t_string.c
void getCommand(client *c) { getGenericCommand(c); }
int getGenericCommand(client *c) {
robj *o = lookupKeyReadOrReply(c, c->argv[1], shared.null[c->resp]);
if (o == NULL) return C_OK;
addReplyBulk(c, o);
return C_OK;
}4. Writing Replies
Before each epoll_wait , beforesleep runs handleClientsWithPendingWrites , which iterates over server.clients_pending_write and attempts to flush each client’s buffer via writeToClient . If the socket’s send buffer cannot accept all data, the client remains in the pending list and a writable event is re‑registered.
//file: src/server.c
void beforeSleep(struct aeEventLoop *eventLoop) {
handleClientsWithPendingWrites();
}
int handleClientsWithPendingWrites(void) {
listIter li; listNode *ln;
listRewind(server.clients_pending_write, &li);
while ((ln = listNext(&li))) {
client *c = listNodeValue(ln);
c->flags &= ~CLIENT_PENDING_WRITE;
listDelNode(server.clients_pending_write, ln);
writeToClient(c->fd, c, 0);
if (clientHasPendingReplies(c)) {
aeCreateFileEvent(server.el, c->fd, ae_flags, sendReplyToClient, c);
}
}
}High‑Performance Summary
Redis achieves tens of thousands of QPS with a single thread by:
Using epoll to multiplex thousands of connections onto one event loop.
Separating connection acceptance, command parsing, execution, and reply buffering into distinct callbacks.
Deferring writes to a pending‑write queue and only registering writable events when the kernel cannot send all data at once.
Understanding initServer and aeMain provides a solid foundation for grasping Redis’s networking architecture.
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.
dbaplus Community
Enterprise-level professional community for Database, BigData, and AIOps. Daily original articles, weekly online tech talks, monthly offline salons, and quarterly XCOPS&DAMS conferences—delivered by industry experts.
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.
