在 Redis 6 之前,Redis 采用的是单线程 io 模型,所有网络请求的接收、解析、命令执行和响应发送,几乎都在主线程中完成。虽然这种模型简单、高效,但在面对高并发场景时,尤其是大量 slow client 或网络瓶颈时,主线程容易成为性能瓶颈。
为了缓解这个问题,Redis 6 引入了 io 多线程机制。具体来说,在读取请求阶段,主线程将已就绪的连接分发给多个 I/O 线程(包括自己),由它们并发读取客户端发送的数据并完成命令解析,然后主线程等待所有 io 线程完成,再统一执行所有命令。同理,在响应阶段,主线程也将写操作交由自己与 io 线程处理。这种“主线程 + io 线程协作”的模型显著降低了网络 io 的开销,提升了在高并发场景下的性能,但是为了避免竞争,实现的比较简单。
到了 Redis 8,io 模型进一步演化。主线程彻底退出 I/O 阶段,每个 io 线程拥有独立的事件循环,独立完成连接监听、请求读取、命令解析与响应写入。只有在需要执行命令时,才将请求通过队列传递给主线程,大大提升了并发处理能力和资源利用率。
事件循环
在 8 版本以前,只有主线程有事件循环,而 8 版本的 io 线程也有了,下面是事件循环的几个重要步骤。
-
执行 beforeSleep(),在下一次阻塞前处理一些事情,例如:正在等待读取数据的客户端、子/主线程通知后等待处理的客户端。
-
等待事件(可能会阻塞),例如 epoll_wait,如果有时间即将到期,那么就阻塞最小的到期时间。
-
执行 afterSleep,(子线程没设置这个函数)。
-
处理文件事件,就是处理第二步返回的网络事件。
-
调用 processTimeEvents 处理时间到期相关的事件。
Redis 6 中的多线程
子线程等待
在 redis 6 中,io 线程的主函数 IOThreadMain 是放在 ionetworking.c 中的。函数中只有 while(1) 死循环,循环中判断 pending 的 client 数量来判断,然后通过一个锁来暂停线程,直到主线程释放对应锁,子线程才会去处理客户端。
void *IOThreadMain(void *myid) {
// ...
while(1) {
/* Wait for start */
for (int j = 0; j < 1000000; j++) {
if (getIOPendingCount(id) != 0) break;
}
/* Give the main thread a chance to stop this thread. */
if (getIOPendingCount(id) == 0) {
// 主线程会持有🔐,这里就被阻塞了,如果有客户端需要处理,会释放这个锁。
pthread_mutex_lock(&io_threads_mutex[id]);
pthread_mutex_unlock(&io_threads_mutex[id]);
continue;
}
serverAssert(getIOPendingCount(id) != 0);
// 遍历分配到自己这里的客户端,分别执行读写操作。
listIter li;
listNode *ln;
listRewind(io_threads_list[id],&li);
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]);
setIOPendingCount(id, 0);
}
}
主线程分配任务
主线程在 io 多线程的模式下,如果事件循环监听到客户端可读事件,不会马上处理,而是留 handleClientsWithPendingWritesUsingThreads以及 handleClientsWithPendingReadsUsingThreads 函数来分配任务,下面以 handleClientsWithPendingReadsUsingThreads 为例,另一个逻辑类似。
void readQueryFromClient(connection *conn) {
// ...
if (postponeClientRead(c)) return;
// ...
}
// 判断是否需要分配给多个线程
// 配置中启用了 io 线程,非主节点、从节点等特殊客户端,io 线程处于空闲状态
int postponeClientRead(client *c) {
if (server.io_threads_active &&
server.io_threads_do_reads &&
!ProcessingEventsWhileBlocked &&
!(c->flags & (CLIENT_MASTER|CLIENT_SLAVE|CLIENT_BLOCKED)) &&
io_threads_op == IO_THREADS_OP_IDLE)
{
listAddNodeHead(server.clients_pending_read,c);
c->pending_read_list_node = listFirst(server.clients_pending_read);
return 1;
} else {
return 0;
}
}
接着,在 beforeSleep 函数中,会调用handleClientsWithPendingReadsUsingThreads 分配待处理的 client。
io_threads_list 变量是一个数组,每一个元素对应一个 io 线程要处理的 clients 列表。
**io_threads_list[0] **对应的是主线程自己。😋
int handleClientsWithPendingReadsUsingThreads(void) {
// ...
listIter li;
listNode *ln;
listRewind(server.clients_pending_read,&li);
int item_id = 0;
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
// 均匀分配 0 - io_threads_num - 1
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);
}
// 干分配给自己的那一部分,调用 readQueryFromClient 读取与解析命令
listRewind(io_threads_list[0],&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
readQueryFromClient(c->conn);
}
listEmpty(io_threads_list[0]);
// 这里是重点,等待所有 io 线程处理完
while(1) {
unsigned long pending = 0;
for (int j = 1; j < server.io_threads_num; j++)
pending += getIOPendingCount(j);
if (pending == 0) break;
}
io_threads_op = IO_THREADS_OP_IDLE;
/* Run the list of clients again to process the new buffers. */
// ..。后续开始执行这些命令。
}
Redis 8 中的多线程
Redis 8 直接让主线程不再处理 io,更加纯粹。主线程与 io 子线程之间使用了链表来转移客户端,使用管道来通信。
线程初始化
Redis 8 则对 io 线程做了大改造,io 线程有了自己独立的文件😄,每个线程都有自己的事件循环。
void *IOThreadMain(void *ptr) {
IOThread *t = ptr;
char thdname[16];
snprintf(thdname, sizeof(thdname), "io_thd_%d", t->id);
redis_set_thread_title(thdname);
redisSetCpuAffinity(server.server_cpulist);
makeThreadKillable();
aeSetBeforeSleepProc(t->el, IOThreadBeforeSleep);
aeMain(t->el); // 启动循环
return NULL;
}
// t->el 是在 initThreadedIO 函数中创建
void initThreadedIO(void) {
if (server.io_threads_num <= 1) return;
server.io_threads_active = 1;
/* Spawn and initialize the I/O threads. */
for (int i = 1; i < server.io_threads_num; i++) {
IOThread *t = &IOThreads[i];
t->id = i;
// 创建事件循环结构体,和主线程的循环基本一致
// 比如 linux 下就会创建 epoll
t->el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
t->el->privdata[0] = t;
// ...
// 与主线程通信的管道。并将管道的读端注册到子线程的循环
// 注意这里的 handleClientsFromMainThread 函数
t->pending_clients_notifier = createEventNotifier();
if (aeCreateFileEvent(t->el, getReadEventFd(t->pending_clients_notifier),
AE_READABLE, handleClientsFromMainThread, t) != AE_OK)
{
serverLog(LL_WARNING, "Fatal: Can't register file event for IO thread notifications.");
exit(1);
}
/* Create IO thread */
if (pthread_create(&t->tid, NULL, IOThreadMain, (void*)t) != 0) {
serverLog(LL_WARNING, "Fatal: Can't initialize IO thread.");
exit(1);
}
// 初始化一些队列
mainThreadPendingClientsToIOThreads[i] = listCreate();
mainThreadPendingClients[i] = listCreate();
mainThreadProcessingClients[i] = listCreate();
pthread_mutex_init(&mainThreadPendingClientsMutexes[i], attr);
mainThreadPendingClientsNotifiers[i] = createEventNotifier();
if (aeCreateFileEvent(server.el, getReadEventFd(mainThreadPendingClientsNotifiers[i]),
AE_READABLE, handleClientsFromIOThread, t) != AE_OK)
{
serverLog(LL_WARNING, "Fatal: Can't register file event for main thread notifications.");
exit(1);
}
if (attr) zfree(attr);
}
}
主线程分发客户端
与 redis 6 不一致的是,在 accept 到 client 后,主线程现将客户端可读事件注册到自己的循环中,这一点和以前一样。
client *createClient(connection *conn) {
client *c = zmalloc(sizeof(client));
// 这里和以前一样,先注册读事件到主线的循环中
if (conn) {
connEnableTcpNoDelay(conn);
if (server.tcpkeepalive)
connKeepAlive(conn,server.tcpkeepalive);
connSetReadHandler(conn, readQueryFromClient); // 这个函数中会注册
connSetPrivateData(conn, c);
}
// ...
}
但是后续多了一个 assignClientToIOThread 步骤,会挑出客户端最少得那个线程分配,保证各个线程之间的客户端均衡。
void clientAcceptHandler(connection *conn) {
client *c = connGetPrivateData(conn);
// .....
// 将客户端分配给 io 线程
if (server.io_threads_num > 1) assignClientToIOThread(c);
}
void assignClientToIOThread(client *c) {
serverAssert(c->tid == IOTHREAD_MAIN_THREAD_ID);
// 找到客户端最少得那个线程
int min_id = 0;
int min = INT_MAX;
for (int i = 1; i < server.io_threads_num; i++) {
if (server.io_threads_clients_num[i] < min) {
min = server.io_threads_clients_num[i];
min_id = i;
}
}
// 由于客户端现在注册在主线程,所以主线程的客户端数量要 -1
server.io_threads_clients_num[c->tid]--;
c->tid = min_id;
c->running_tid = min_id;
server.io_threads_clients_num[min_id]++; // 目标线程 +1
// 从主线程的事件循环中删除对应的事件,调用 epoll_ctl,这个时候,客户端没有监听任何事件。
connUnbindEventLoop(c->conn);
c->io_flags &= ~(CLIENT_IO_READ_ENABLED | CLIENT_IO_WRITE_ENABLED);
// 将客户端挂到 mainThreadPendingClientsToIOThreads 对应的线程中
listAddNodeTail(mainThreadPendingClientsToIOThreads[c->tid], c);
}
紧接着,在主线程的 beforeSleep 函数中,会调用sendPendingClientsToIOThreads通知子线程。
int sendPendingClientsToIOThreads(void) {
int processed = 0;
for (int i = 1; i < server.io_threads_num; i++) {
int len = listLength(mainThreadPendingClientsToIOThreads[i]);
if (len > 0) {
IOThread *t = &IOThreads[i];
pthread_mutex_lock(&t->pending_clients_mutex);
// 将mainThreadPendingClientsToIOThreads中的客户端塞入对应线程的 pending_clients
listJoin(t->pending_clients, mainThreadPendingClientsToIOThreads[i]);
pthread_mutex_unlock(&t->pending_clients_mutex);
// 往管道中写一个字节内容
triggerEventNotifier(t->pending_clients_notifier);
}
processed += len;
}
return processed;
}
int triggerEventNotifier(struct eventNotifier *en) {
#ifdef HAVE_EVENT_FD
uint64_t u = 1;
if (write(en->efd, &u, sizeof(uint64_t)) == -1) {
return EN_ERR;
}
#else
char buf[1] = {'R'};
if (write(en->pipefd[1], buf, 1) == -1) {
return EN_ERR;
}
#endif
return EN_OK;
}
子线程接收客户端
前面子线程初始化的时候,handleClientsFromMainThread 函数被注册到了管道的读端,所以主线程发送一个字节消息的时候,子线程会调用 handleClientsFromMainThread 函数
handleClientsFromMainThread 会遍历自身的 pending_clients 中的客户端,将他们重新注册到自己的事件循环中。
void handleClientsFromMainThread(struct aeEventLoop *ae, int fd, void *ptr, int mask) {
IOThread *t = ptr;
// 将 pending_clients 中的客户端挂到 processing_clients
// 表示正在处理的 客户端
pthread_mutex_lock(&t->pending_clients_mutex);
listJoin(t->processing_clients, t->pending_clients);
pthread_mutex_unlock(&t->pending_clients_mutex);
if (listLength(t->processing_clients) == 0) return;
listIter li;
listNode *ln;
listRewind(t->processing_clients, &li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
// 将客户端塞到 clients 数组
listAddNodeTail(t->clients, c);
c->io_thread_client_list_node = listLast(t->clients);
// ...
/* Enable read and write and reset some flags. */
c->io_flags |= CLIENT_IO_READ_ENABLED | CLIENT_IO_WRITE_ENABLED;
c->io_flags &= ~CLIENT_IO_PENDING_COMMAND;
// 重新将客户端绑定注册到自己的事件循环
if (!connHasEventLoop(c->conn)) {
connRebindEventLoop(c->conn, t->el);
serverAssert(!connHasReadHandler(c->conn));
connSetReadHandler(c->conn, readQueryFromClient);
}
// 如果客户端有待写的内容,那么就写
if (clientHasPendingReplies(c)) {
writeToClient(c, 0);
if (!(c->io_flags & CLIENT_IO_CLOSE_ASAP) && clientHasPendingReplies(c)) {
connSetWriteHandler(c->conn, sendReplyToClient);
}
}
}
listEmpty(t->processing_clients);
}
通知主线程
6 - 8 版本都是通过 readQueryFromClient 函数读取数据,然后调用 processInputBuffer 函数解析命令。区别就是 8 版本在processInputBuffer函数解析完成后调用的是enqueuePendingClientsToMainThread来处理后续的命令执行环节 ,如果是主线程,那么就直接调用 processCommandAndResetClient 函数执行命令。
enqueuePendingClientsToMainThread 函数将客户端塞入子线程自身的pending_clients_to_main_thread链表中
void enqueuePendingClientsToMainThread(client *c, int unbind) {
/* If the IO thread may no longer manage it, such as closing client, we should
* unbind client from event loop, so main thread doesn't need to do it costly. */
if (unbind) connUnbindEventLoop(c->conn);
/* Just skip if it already is transferred. */
if (c->io_thread_client_list_node) {
listDelNode(IOThreads[c->tid].clients, c->io_thread_client_list_node);
c->io_thread_client_list_node = NULL;
/* Disable read and write to avoid race when main thread processes. */
c->io_flags &= ~(CLIENT_IO_READ_ENABLED | CLIENT_IO_WRITE_ENABLED);
listAddNodeTail(IOThreads[c->tid].pending_clients_to_main_thread, c);
}
}
紧接着在 IOThreadBeforeSleep 函数中,会把 pending_clients_to_main_thread 链表中的客户端塞进 mainThreadPendingClients 中,然后通知主线程
void IOThreadBeforeSleep(struct aeEventLoop *el) {
IOThread *t = el->privdata[0];
// ...
if (listLength(t->pending_clients_to_main_thread) > 0) {
pthread_mutex_lock(&mainThreadPendingClientsMutexes[t->id]);
listJoin(mainThreadPendingClients[t->id], t->pending_clients_to_main_thread);
pthread_mutex_unlock(&mainThreadPendingClientsMutexes[t->id]);
// 通知主线程
triggerEventNotifier(mainThreadPendingClientsNotifiers[t->id]);
}
}
主线程执行命令
主线程收到子线程的事件后,会触发 handleClientsFromIOThread 函数,
void handleClientsFromIOThread(struct aeEventLoop *el, int fd, void *ptr, int mask) {
IOThread *t = ptr;
/// ...
/* Get the list of clients to process. */
pthread_mutex_lock(&mainThreadPendingClientsMutexes[t->id]);
listJoin(mainThreadProcessingClients[t->id], mainThreadPendingClients[t->id]);
pthread_mutex_unlock(&mainThreadPendingClientsMutexes[t->id]);
if (listLength(mainThreadProcessingClients[t->id]) == 0) return;
// 处理 client,执行命令
processClientsFromIOThread(t);
}
processClientsFromIOThread函数会执行命令,接着如果客户端有待写的内容,那么就把客户端丢到 mainThreadPendingClientsToIOThreads 链表中
然后通过触发对应线程的 pending_clients_notifier 通知子线程
pending_clients_notifier对应的事件回调函数前面提到过,在末尾会将待回复的内容写入 socket。
总结
Redis 的 I/O 模型在 6.0 和 8.0 版本中经历了重大演进,提升高并发环境下的网络处理性能。Redis 6 引入了多线程 I/O 模式,将原本由主线程独揽的网络读写操作拆分给多个 I/O 子线程执行。主线程负责事件监听及命令执行,而具体的数据读取与响应发送则由主线程与子线程并行处理。这种“协作式并发”显著缓解了主线程的压力,特别是在 slow client 较多的场景中效果明显。
在实现上,Redis 6 中的 I/O 线程通过一个简单的死循环不断检查是否有待处理的 client,并在主线程通过锁唤醒后执行读或写操作。而主线程在事件循环的 beforeSleep 阶段,通过如 handleClientsWithPendingReadsUsingThreads 的逻辑分发任务给子线程,并等待所有子线程处理完成,最后统一执行命令。
到了 Redis 8,这一模型更进一步优化。I/O 子线程不再依赖主线程统一调度,而是拥有独立的事件循环和连接管理能力。主线程在接收到连接后,按照负载均衡策略将 client 分发给子线程,并通过管道进行通知。每个子线程完全独立地监听事件、读取请求、解析命令并写入响应,极大增强了系统的并发处理能力。