AIGC标识 redis-源码带读-02-事件驱动与网络模型

Redis 源码带读 第2篇:事件驱动与网络模型

本篇目标

理解Redis单线程事件循环的核心机制,包括ae事件抽象层、epoll实现、网络处理和客户端生命周期。

前置知识

  • 了解epoll的基本原理
  • 了解非阻塞I/O和I/O多路复用
  • 阅读过第1篇

1. ae.c 事件循环抽象层

1.1 核心数据结构

// ae.h:79
typedef struct aeEventLoop {
    int maxfd;                    // 已注册的最大文件描述符
    int setsize;                  // 最大文件描述符数
    long long timeEventNextId;    // 下一个时间事件ID
    aeFileEvent *events;          // 文件事件数组(按fd索引)
    aeFiredEvent *fired;          // 已触发事件数组
    aeTimeEvent *timeEventHead;   // 时间事件链表头(无序)
    int stop;                     // 是否停止
    int nevents;                  // 事件数组大小(惰性增长)
    void *apidata;                // 平台特定数据(epoll fd等)
    aeBeforeSleepProc *beforesleep; // 睡眠前回调
    aeBeforeSleepProc *aftersleep;  // 睡眠后回调
} aeEventLoop;

1.2 文件事件(File Event)

// ae.h:52
typedef struct aeFileEvent {
    int mask;                   // AE_READABLE | AE_WRITABLE | AE_BARRIER
    aeFileProc *rfileProc;      // 读回调
    aeFileProc *wfileProc;      // 写回调
    void *clientData;
} aeFileEvent;

1.3 时间事件(Time Event)

// ae.h:60
typedef struct aeTimeEvent {
    long long id;               // 事件ID
    monotime when;              // 触发时间(微秒,单调时钟)
    aeTimeProc *timeProc;       // 回调函数
    struct aeTimeEvent *prev;   // 双向链表
    struct aeTimeEvent *next;
    int refcount;               // 引用计数(防止递归释放)
} aeTimeEvent;

1.4 核心函数

函数 行数 作用
aeCreateEventLoop(setsize) 47 创建事件循环
aeCreateFileEvent(el,fd,mask,proc,data) 145 注册文件事件
aeDeleteFileEvent(el,fd,mask) 176 删除文件事件
aeCreateTimeEvent(el,milliseconds,proc,data,finalizerProc) 218 创建时间事件
aeMain(el) 497 进入事件循环
aeProcessEvents(el,flags) 365 处理就绪事件

1.5 事件循环优化

Redis对事件循环进行了多项优化:

  1. 惰性增长:事件数组初始只分配1024个槽位,按需增长
  2. 时间事件前置插入:O(1)插入到链表头部
  3. 懒删除:时间事件删除只标记ID为AE_DELETED_EVENT_ID,在处理时清理
  4. 内存预分配:使用zmalloc_usable利用jemalloc的size class

2. 主循环流程

2.1 aeMain()

// ae.c:497
void aeMain(aeEventLoop *eventLoop) {
    while (!eventLoop->stop) {
        // 1. 睡眠前处理(刷新缓冲区、处理待关闭客户端等)
        if (eventLoop->beforesleep)
            eventLoop->beforesleep(eventLoop);

        // 2. 处理事件(epoll_wait + 回调)
        aeProcessEvents(eventLoop, AE_ALL_EVENTS | AE_CALL_BEFORE_SLEEP | AE_CALL_AFTER_SLEEP);
    }
}

2.2 aeProcessEvents()

// ae.c:365
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
    int processed = 0;

    // 1. 调用beforesleep
    if (eventLoop->beforesleep && (flags & AE_CALL_BEFORE_SLEEP))
        eventLoop->beforesleep(eventLoop);

    // 2. 计算最近时间事件的超时时间
    tvp = usUntilEarliestTimer(eventLoop);  // O(N)扫描

    // 3. 调用epoll_wait(或kqueue/select)
    numevents = aeApiPoll(eventLoop, tvp);

    // 4. 调用aftersleep(恢复IO线程、更新时间)
    if (eventLoop->aftersleep && (flags & AE_CALL_AFTER_SLEEP))
        eventLoop->aftersleep(eventLoop);

    // 5. 处理就绪事件
    for (j = 0; j < numevents; j++) {
        int fd = eventLoop->fired[j].fd;
        int mask = eventLoop->fired[j].mask;

        // 先处理读事件(可能产生写数据,如同步写回复)
        if (fd != -1 && mask & AE_READABLE && !reverse) {
            eventLoop->events[fd].rfileProc(eventLoop, fd,
                eventLoop->events[fd].clientData, mask);
        }

        // 再处理写事件
        if (fd != -1 && mask & AE_WRITABLE) {
            eventLoop->events[fd].wfileProc(eventLoop, fd,
                eventLoop->events[fd].clientData, mask);
        }

        processed++;
    }

    // 6. 处理时间事件
    processTimeEvents(eventLoop);

    return processed;
}

2.3 事件处理顺序

Redis的事件处理顺序经过精心设计:

epoll_wait返回
  |
  +-> 先处理读事件(AE_READABLE)
  |     -> 可能产生写数据(同步写回复)
  |
  +-> 再处理写事件(AE_WRITABLE)
  |     -> 发送缓冲区中的回复
  |
  +-> 处理时间事件
        -> serverCron(过期键删除、统计等)

AE_BARRIER标志:用于从服务器和AOF重写场景,反转读写处理顺序,确保先完成写操作再处理读事件。


3. epoll实现:ae_epoll.c

3.1 平台抽象

ae.c通过函数指针实现平台无关性:

// ae_epoll.c
static int aeApiCreate(aeEventLoop *eventLoop);
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask);
static int aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask);
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp);
static int aeApiFree(aeEventLoop *eventLoop);
static int aeApiResize(aeEventLoop *eventLoop, int setsize);

3.2 epoll系统调用映射

ae函数 epoll调用 说明
aeApiCreate epoll_create(1024) 创建epoll实例
aeApiAddEvent epoll_ctl(ADD) 注册事件
aeApiDelEvent epoll_ctl(DEL) 删除事件
aeApiPoll epoll_wait() 等待就绪事件

3.3 epoll实现细节

// ae_epoll.c
static int aeApiCreate(aeEventLoop *eventLoop) {
    aeApiState *state = zmalloc(sizeof(aeApiState));
    state->events = zmalloc(sizeof(struct epoll_event) * eventLoop->setsize);
    state->epfd = epoll_create(1024);
    eventLoop->apidata = state;
    return 0;
}

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee = {0};

    ee.events = 0;
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.fd = fd;

    // EPOLL_CTL_ADD 或 EPOLL_CTL_MOD
    if (state->events[fd].events == 0) {
        epoll_ctl(state->epfd, EPOLL_CTL_ADD, fd, &ee);
    } else {
        epoll_ctl(state->epfd, EPOLL_CTL_MOD, fd, &ee);
    }
    return 0;
}

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int timeout;

    if (tvp != NULL) {
        timeout = (tvp->tv_sec * 1000) + (tvp->tv_usec / 1000);
    } else {
        timeout = -1;  // 无限等待
    }

    // 调用epoll_wait
    int nevents = epoll_wait(state->epfd, state->events, eventLoop->setsize, timeout);

    // 将epoll事件转换为fired事件
    for (int j = 0; j < nevents; j++) {
        int mask = 0;
        if (state->events[j].events & EPOLLIN) mask |= AE_READABLE;
        if (state->events[j].events & EPOLLOUT) mask |= AE_WRITABLE;
        if (state->events[j].events & EPOLLERR) mask |= AE_READABLE | AE_WRITABLE;
        if (state->events[j].events & EPOLLHUP) mask |= AE_READABLE | AE_WRITABLE;

        eventLoop->fired[j].fd = state->events[j].data.fd;
        eventLoop->fired[j].mask = mask;
    }

    return nevents;
}

4. 网络层:networking.c

4.1 客户端生命周期

连接到达
  -> acceptTcpHandler() [ae事件回调]
    -> connCreateAcceptedSocket()
    -> createClient()
      -> 分配redisClient结构
      -> 初始化查询缓冲区(16KB)
      -> aeCreateFileEvent(fd, AE_READABLE, readQueryFromClient)

读取命令
  -> readQueryFromClient()
    -> read(fd, c->querybuf, ...)  读取数据
    -> processInputBuffer(c)        解析RESP协议
      -> processMultibulkBuffer()   解析*3\r\n$3\r\nSET\r\n...
      -> c->argv[0] = "SET"
      -> c->argv[1] = "key"
      -> c->argv[2] = "value"
    -> processCommand(c)            执行命令

写入回复
  -> writeToClient()
    -> write(fd, c->buf, c->bufpos)  发送响应
    -> 如果写不完:注册AE_WRITABLE事件,下次继续

关闭连接
  -> freeClient()
    -> aeDeleteFileEvent(fd, ...)
    -> 释放querybuf、reply等
    -> 从clients列表中移除

4.2 readQueryFromClient()

// networking.c:3982
void readQueryFromClient(connection *conn) {
    client *c = connGetPrivateData(conn);
    int nread, big_arg = 0;

    // 1. 检查是否被IO线程拥有
    if (c->running_tid != -1 && c->running_tid != MAIN_THREAD_ID) {
        c->pending_read = 1;  // 延迟到主线程处理
        return;
    }

    // 2. 可复用查询缓冲区(避免每次都分配)
    if (c->querybuf == NULL) {
        c->querybuf = sdsnewlen(SDS_NOINIT, PROTO_IOBUF_LEN);
    }

    // 3. 检查缓冲区大小
    querybuf_size = sdsavail(c->querybuf);
    if (c->querybuf_peak > querybuf_size) {
        querybuf_size = c->querybuf_peak;
    }

    // 4. 读取数据
    nread = connRead(conn, c->querybuf + qblen, readlen);

    if (nread == -1) {
        if (errno == EAGAIN) return;  // 没有更多数据
        // 其他错误,关闭连接
        freeClientAsync(c);
        return;
    }

    // 5. 更新统计
    c->net_input_bytes += nread;

    // 6. 解析输入
    processInputBuffer(c);
}

4.3 processInputBuffer()

// networking.c:3778
int processInputBuffer(client *c) {
    while (sdslen(c->querybuf) - c->qb_pos > 0) {
        // 1. 检查是否需要更多数据
        if (!c->reqtype) {
            // 根据第一个字节判断协议类型
            if (c->querybuf[c->qb_pos] == '*') {
                c->reqtype = PROTO_REQ_MULTIBULK;  // RESP协议
            } else {
                c->reqtype = PROTO_REQ_INLINE;      // 内联协议
            }
        }

        // 2. 解析命令
        if (c->reqtype == PROTO_REQ_MULTIBULK) {
            if (processMultibulkBuffer(c) != C_OK) break;
        } else {
            if (processInlineBuffer(c) != C_OK) break;
        }

        // 3. 执行命令
        if (processCommandAndResetClient(c) != C_OK) {
            return C_ERR;  // 客户端已释放
        }
    }

    // 4. 清理已消费的数据
    if (c->qb_pos > 0) {
        sdsrange(c->querybuf, c->qb_pos, -1);
        c->qb_pos = 0;
    }

    return C_OK;
}

4.4 writeToClient()

// networking.c:2935
int writeToClient(client *c, int handler_installed) {
    ssize_t nwritten = 0;

    // 1. 发送缓冲区中的回复
    if (c->bufpos > 0) {
        nwritten = connWrite(c->conn, c->buf, c->bufpos);
        if (nwritten <= 0) {
            // 写入失败或缓冲区满
            return C_OK;
        }
        c->bufpos -= nwritten;
        memmove(c->buf, c->buf + nwritten, c->bufpos);
    }

    // 2. 发送reply链表中的回复
    while (listLength(c->reply) > 0) {
        listNode *ln = listFirst(c->reply);
        clientReplyBlock *reply = listNodeValue(ln);

        nwritten = connWrite(c->conn, reply->bytes + c->repl_offset, reply->size - c->repl_offset);
        if (nwritten <= 0) break;

        c->repl_offset += nwritten;
        if (c->repl_offset == reply->size) {
            listDelNode(c->reply, ln);
            c->repl_offset = 0;
        }
    }

    // 3. 检查是否发送完成
    if (c->bufpos == 0 && listLength(c->reply) == 0) {
        c->reply_bytes = 0;
        // 移除写事件
        aeDeleteFileEvent(server.el, c->fd, AE_WRITABLE);

        // 检查是否需要关闭连接
        if (c->flags & CLIENT_CLOSE_AFTER_REPLY) {
            freeClient(c);
            return C_ERR;
        }
    }

    return C_OK;
}

5. RESP协议解析

5.1 RESP协议格式

*3\r\n           -> 3个元素的数组
$3\r\n           -> 3字节的字符串
SET\r\n          -> "SET"
$3\r\n
key\r\n          -> "key"
$5\r\n
hello\r\n        -> "hello"

5.2 RESP解析器

resp_parser.c 实现了RESP解析器,将网络字节流解析为命令参数数组。

解析流程:

  1. 读取第一行,确定类型(*数组、+简单字符串、:整数、$批量字符串、-错误)
  2. 如果是数组,递归解析每个元素
  3. 将解析结果存储到 c->argv 数组中

5.3 内联协议

Redis也支持内联协议(Inline Protocol),用于telnet等简单客户端:

SET key value\r\n
GET key\r\n

内联协议通过空格分隔参数,比RESP协议简单但不支持二进制数据。


6. 非阻塞I/O

Redis在使用非阻塞I/O:

// anet.c
int anetNonBlock(int fd) {
    return anetSetBlock(fd, 1); // 1=非阻塞
}

结合epoll实现I/O多路复用:主线程不阻塞在任何单个socket上,而是同时监控所有连接,只处理就绪的连接。

6.1 非阻塞I/O的优势

  1. 不阻塞:单个连接的慢速读写不会影响其他连接
  2. 高效:主线程只在epoll_wait时阻塞,处理事件时完全不阻塞
  3. 可扩展:可以同时处理数万个连接

6.2 非阻塞I/O的挑战

  1. 需要处理EAGAIN:读取/写入可能返回EAGAIN,需要稍后重试
  2. 需要缓冲区管理:数据可能分多次到达,需要拼接
  3. 需要事件驱动:写入缓冲区满时需要注册写事件,可写时继续发送

7. IO线程(Redis 6.0+)

Redis 6.0引入了多线程I/O,用于网络读写:

7.1 IO线程工作原理

主线程:
  -> 检查是否有就绪的IO线程
    -> 如果有:从IO线程获取解析好的命令
    -> 执行命令
    -> 将回复放入IO线程的待发送队列

IO线程(最多128个):
  -> 读取客户端数据
  -> 解析RESP协议
    -> 将解析好的命令放入待执行队列

写操作:
  -> 主线程将回复放入IO线程的待发送队列
  -> IO线程执行实际的write操作

7.2 IO线程配置

# 启用IO线程
io-threads 4

# 是否启用IO线程读取
io-threads-do-reads yes

7.3 IO线程的限制

  • 命令执行仍然是单线程
  • IO线程只处理网络读写和RESP解析
  • 避免了多线程命令执行的锁竞争问题

8. 本篇小结

概念 要点
事件循环 aeMain -> aeProcessEvents -> aeApiPoll(epoll_wait)
文件事件 fd + mask + 读写回调,epoll管理
时间事件 serverCron,每秒10次定时任务
网络层 accept -> read -> process -> write -> free
RESP协议 简单文本协议,*N\r\n$len\r\ndata\r\n
平台抽象 函数指针切换epoll/kqueue/select
IO线程 Redis 6.0+,多线程I/O,单线程执行

思考题

  1. 为什么Redis使用单线程处理命令而不是多线程?(提示:锁竞争、数据结构简化)
  2. serverCron为什么默认是每秒10次而不是1次或100次?
  3. 如果客户端发送了一个非常大的命令(1GB),Redis会如何处理?
  4. epoll的ET(边缘触发)和LT(水平触发)模式对Redis有什么影响?

思考题解答

1. 为什么Redis使用单线程处理命令,而不是多线程?

单线程的优势

  1. 命令串行化:单线程执行所有命令,天然保证操作的原子性和顺序性,不需要加锁。
  2. 数据结构简化:不需要考虑线程安全,数据结构设计更简单高效。
  3. 避免锁竞争:多线程需要细粒度的锁,锁竞争会严重影响性能。
  4. 无上下文切换:单线程没有线程切换开销。

多线程的问题

  • 锁竞争:共享数据结构需要加锁,锁粒度难以把握
  • 死锁风险:多线程环境下死锁难以调试
  • 内存屏障:需要使用内存屏障保证可见性,增加复杂度

Redis 6.0的改进:引入多线程I/O,但命令执行仍然是单线程。多线程只用于网络读写(解析和序列化RESP协议),不用于命令处理。

2. serverCron为什么默认每秒运行10次(1次100ms)?

原因

  1. 及时性:100ms的间隔足够及时地检测和处理各种状态(过期键、统计更新等)
  2. 性能平衡:如果太频繁(如1ms),会增加CPU开销;如果太慢(如10s),状态更新不及时
  3. 历史经验:经过大量测试验证,100ms是一个性能和及时性的最佳平衡点

serverCron的主要任务

  • 每100ms:更新LRU时钟、检查过期键、统计更新
  • 每5秒:检查内存使用、持久化状态
  • 每1分钟:执行内存碎片整理
  • 每1小时:检查备份状态

3. 当客户端发送一个非常大的命令(1GB)时,Redis会如何处理?

Redis的处理流程

  1. 内存分配:Redis会尝试分配1GB的内存来存储这个命令
  2. 内存检查:如果 maxmemory 限制被超过,Redis会根据淘汰策略处理
  3. 阻塞风险:大命令会阻塞整个服务器,因为Redis是单线程的

可能的结果

  • 如果有足够内存:分配成功,但会导致其他客户端等待
  • 如果内存不足:触发OOM错误或淘汰key
  • 如果 timeout 设置:客户端超时断开

建议

  • 设置 client-query-buffer-limit 限制客户端输入缓冲区大小
  • 使用 timeout 设置客户端超时时间
  • 避免执行大key操作(如 KEYS *、大hash的 HGETALL

4. epoll的ET边缘触发模式对比LT水平触发模式,对Redis有什么影响?

ET(边缘触发)vs LT(水平触发)

特性 ET(边缘触发) LT(水平触发)
触发条件 状态变化时触发 只要条件满足就触发
事件丢失 可能丢失(需一次性读完) 不会丢失
系统调用 更少(减少epoll_wait返回) 更多
编程复杂度 高(需处理EAGAIN)

Redis使用ET模式

  • 减少epoll_wait返回次数,提高性能
  • 必须一次性读取所有数据(直到EAGAIN),否则可能丢失事件
  • Redis的readQueryFromClient函数会循环读取直到EAGAIN

代码实现

// ae.c
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    events = epoll_wait(eventLoop->apidata.epfd,
                       eventLoop->apidata.events,
                       eventLoop->apidata.setsize,
                       timeout);
    // ET模式:必须一次性处理所有事件
    for (j = 0; j < events; j++) {
        // 处理事件
    }
}

建议:ET模式性能更好,但需要更小心地处理事件。Redis已经正确实现了ET模式。

posted @ 2026-09-04 17:26  IcarusLee  阅读(2)  评论(0)    收藏  举报