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

相关推荐
邓工说电27 分钟前
智慧断路器安全吗?数据加密、离线保护与合规认证全解读
大数据·数据库·人工智能·智能断路器·炜晔科技
xh didida43 分钟前
Linux -- 基础IO
linux·服务器·开发语言·c++
adinnet202644 分钟前
企业微调大模型(Fine-tuning):先吃透 RAG 还是先训自己的模型
大数据·数据库
行百里er1 小时前
Redis Streams——有确认、能回溯的消息队列
redis
心易行者1 小时前
Agent应用+API端点商业化进阶实战:从单体智能体到可付费调用的API全流程
运维·服务器·人工智能·python·apache
上位机妹子1 小时前
EF Core 两种继承映射:每层次一张表(TPH)与每类型一张表(TPT)
数据库·c#·.netcore·ef core
小马同学-2 小时前
Redis消息队列与客户端编程
数据库·redis
夜之眷属2 小时前
Core dump 崩溃排查:JVM 宕机后,那份 core 文件怎么用 gdb 还原现场
java·运维·服务器·jvm
zly35002 小时前
centos7 mount设备挂载 /dev/sda1/硬盘挂载
linux·运维·服务器
for_ever_love__2 小时前
MySQL 高频面试题 30 问:索引、事务、锁、日志、主从与分库分表(MySQL 8.0 版)
java·数据库·spring boot·mysql·spring cloud·面试·总结