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 的事件处理模型。

相关推荐
名字还没想好☜3 小时前
Python f-string 进阶:数字格式化、对齐填充、调试 = 号与嵌套表达式
开发语言·数据库·python·字符串格式化·f-string
ltl4 小时前
Serverless 数据库弹性理论:Neon 与 Aurora Serverless v2
数据库
·薯条大王4 小时前
经济实惠玩云服务器|一台云服务器多人共用,子账号配置教程
java·linux·运维·服务器·汇编·c++·python
ZJH__GO7 小时前
网络编程v4pro--实现聊天室文件传输功能
java·服务器·网络·计算机网络
小小、码农7 小时前
Linux进程控制:从fork到exec,手写一个简易Shell
linux·运维·服务器
不会就选b7 小时前
Linux之进程间通信(二)---模拟进程池
linux·运维·服务器
marvelyu8 小时前
每天10分钟学会OceanBase系列(Day 20):跨机房容灾实战——构建多数据中心高可用架构
java·大数据·数据库
NeilYuen8 小时前
用UDP实现向DNS服务器请求IP地址
服务器·tcp/ip·udp
ZCBUS实时计算8 小时前
金融证券实时数仓建设实践:轻量化实时计算平台落地,实现交易数据端到端秒级处理
大数据·数据库·数据仓库·金融·flink·dba·etl
zd2005729 小时前
海洋微生物数据库
数据库·宏基因组