Redis 服务器线程与事件循环解析

文章目录

事件循环线程

众所周知,Redis 服务器启动时会开启六个线程:

  1. Thread-1 [redis-server](主线程)
  2. Thread-2 [bio_close_file](后台 I/O 线程 - 文件关闭)
  3. Thread-3 [bio_aof](后台 I/O 线程 - AOF 刷盘)
  4. Thread-4 [bio_lazy_free](后台 I/O 线程 - 惰性删除)
  5. Thread-5 [jemalloc_bg_thd](内存整理线程)
  6. Thread-6 [jemalloc_bg_thd](内存整理线程)

而事件循环则位于 Thread-1 [redis-server](主线程)中。

事件循环相关源码文件

源码文件 说明
ae.c 事件驱动循环核心
ae_epoll.c Linux 系统实现(epoll)
ae_evport.c Solaris 专用
ae_kqueue.c macOS 专用
ae_select.c 老旧 Linux 或兼容模式使用
ebuckets.c 事件桶管理
eventnotifier.c 事件通讯器

事件主循环机制

Redis 的事件主循环位于 ae.c 中,核心代码如下:

c 复制代码
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|
                                   AE_CALL_BEFORE_SLEEP|
                                   AE_CALL_AFTER_SLEEP);
    }
}

但死循环非常消耗 CPU,Redis 是如何让线程进入睡眠等待的呢?

真正的"睡眠"发生在 aeApiPoll 中,其核心是调用系统 epoll_wait

c 复制代码
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    retval = epoll_wait(state->epfd, state->events, eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + (tvp->tv_usec + 999)/1000) : -1);
    if (retval > 0) {
        int j;
        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events + j;

            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE|AE_READABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE|AE_READABLE;
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    } else if (retval == -1 && errno != EINTR) {
        panic("aeApiPoll: epoll_wait, %s", strerror(errno));
    }
    return numevents;
}
  • epoll_wait 就是实现"睡眠"的系统调用。
  • 当没有事件时,线程在此阻塞,不消耗 CPU。

睡眠钩子(Hooks)机制

为了在睡眠前后执行必要逻辑,Redis 提供了钩子函数。

initServer 函数中注册:

复制代码
aeSetBeforeSleepProc(server.el, beforeSleep);
aeSetAfterSleepProc(server.el, afterSleep);

aeProcessEvents 中调用:

复制代码
if (eventLoop->beforesleep != NULL && (flags & AE_CALL_BEFORE_SLEEP))
    eventLoop->beforesleep(eventLoop);

if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
    eventLoop->aftersleep(eventLoop);

作用

  • beforeSleep:处理客户端写回、检查阻塞超时、计算下一次 epoll_wait 的超时时间等。
  • afterSleep:清理或重置休眠期间的状态。

这些钩子避免了无限循环中 CPU 空转,是 Redis 高效的核心之一。


睡眠钩子的实现

beforeSleep钩子和afterSleep钩子的实现都在server.c中,分别是void beforeSleep(struct aeEventLoop *eventLoop)和void afterSleep(struct aeEventLoop *eventLoop)。

实现上beforeSleep远比afterSleep复杂。

Epoll 实战:键盘输入监听

我们可以编写代码练习 epoll 的使用,模拟 Redis 的事件监听机制。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <unistd.h>

// 简单的错误处理宏
#define panic(fmt, ...) \
    do { fprintf(stderr, "FATAL ERROR: " fmt " (%s:%d)\n", ##__VA_ARGS__, __FILE__, __LINE__); \
    exit(1); } while(0)

typedef struct epoll_event Event;

int main(void) {
    int epfd = epoll_create(1);
    if (epfd < 0) {
        panic("epoll_create");
        return 1;
    }

    // 设置监听标准输入(键盘)
    Event ev;
    ev.events = EPOLLIN;
    ev.data.fd = STDIN_FILENO;

    if (epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev) < 0) {
        panic("epoll_ctl");
        return 1;
    }

    Event events[10];
    char buf[1024];

    // 事件循环
    for (;;) {
        int nfds = epoll_wait(epfd, events, 10, -1);
        if (nfds < 0) {
            panic("epoll_wait");
            return 1;
        }

        // 处理就绪事件
        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == STDIN_FILENO) {
                int nread = read(STDIN_FILENO, buf, sizeof(buf));
                if (nread > 0) {
                    buf[nread] = '\0';
                    printf("键盘输入: %s", buf);
                }
            }
        }
    }

    close(epfd);
    return 0;
}

编译运行

shell 复制代码
gcc epoll_test.c -o epoll_test
./epoll_test

这个程序会在没有输入时"睡眠",输入内容按回车后被唤醒,完美模拟了 Redis 的事件处理模型。

相关推荐
三无少女指南6 分钟前
开发者环境配置:用 Ollama 实现本地大模型部署(附下载慢的解决方案
c语言·开发语言·数据库·ubuntu
夏乌_Wx10 分钟前
mybash:简易 Shell 实现的设计思路与核心模块解析
linux·服务器·前端
Lxinccode1 小时前
AI编程(3) / claude code[3] : 更新apiKey
java·数据库·ai编程·claude code
vortex51 小时前
APT软件包管理从入门到精通
linux·运维·服务器·kali
古译汉书1 小时前
RTOS:ISR与互斥量的关系
运维·服务器·stm32·嵌入式硬件
数据知道1 小时前
MongoDB数据类型全景:String、Number、Date、Boolean 及特殊的 null 类型处理
数据库·mongodb
feng68_1 小时前
Keepalived基础实现
运维·服务器·keepalived
暴力求解1 小时前
Linux---基础IO详解
linux·运维·服务器
醉颜凉2 小时前
PostgreSQL 模式(SCHEMA)详解:数据库对象的命名空间管理
数据库·postgresql
AI题库2 小时前
PostgreSQL 18 默认密码修改全指南:从安装到安全加固
数据库·安全·postgresql