掘金 后端 ( ) • 2024-04-03 13:22

Redis为什么这么快?这个经典的面试题中答案包含以下几点:

  1. 内存存储:所有的数据都存储在内存中,这使得数据的读写速度非常快

  2. 数据结构简单直接:Redis 提供了一些高效的数据结构,如字符串、列表、集合、哈希表和有序集合。这些数据结构被直接实现在内存中,操作它们的时间复杂度通常很低

  3. 单线程模型:Redis 利用单线程模型来处理客户端的请求,避免了多线程的上下文切换和竞争条件,这样可以确保操作的原子性和一致性,同时提高了性能

  4. I/O多路复用技术:I/O 多路复用技术来同时监视多个套接字,并在套接字准备好读取或写入时通知 Redis

  5. 优化的协议:Redis 协议简单而紧凑,减少了网络传输的数据量

    持久化策略、高效的发布/订阅和事务支持、可调的数据淘汰策略、优化的底层实现(跳表)、主从复制和分片等等一系列技术手段

今天这边主要介绍其中的I/O多路复用技术及底层代码实现。

Redis 使用 I/O 多路复用技术来同时处理多个网络连接上的 I/O 操作。

I/O 多路复用是一种允许单个线程监视多个文件描述符(在网络编程中通常是套接字)一旦其中一个或多个文件描述符准备好进行 I/O 操作(例如读取或写入),系统就会通知程序。

多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,变成了一次系统调用 + 内核层遍历这些文件描述符

常见的 I/O 多路复用技术:

  1. select:最基本的 I/O 多路复用接口。它允许程序监视多个文件描述符,等待一个或多个文件描述符成为“就绪”状态。

  2. poll:与 select 类似,但没有文件描述符数量的限制。

  3. epoll(仅限 Linux):比 select 和 poll 更高效,没有文件描述符数量的限制,支持“水平触发”和“边缘触发”。

  4. kqueue(仅限 BSD 系统):类似于 epoll,但是用于 BSD 系统。

    Redis 根据操作系统选择最合适的 I/O 多路复用 API。在 Linux 上,它通常使用 epoll;在 macOS 上,它使用 kqueue。

    这里我们将主要关注 epoll,因为它是在 Linux 系统上 Redis 常用的 I/O 多路复用技术。以下是 Redis 中 I/O 多路复用的核心源码的简化解析

事件处理器的初始化:

在 ae.c 中,Redis 初始化事件处理器并设置相应的多路复用库。

图片

aeApiCreate 函数中,Redis 根据编译时确定的系统类型来调用相应的初始化函数,例如在 Linux 上,它会调用 ae_epoll.c 中的 aeApiCreate

ae_epoll.c 中,Redis 实现了对 epoll 的封装。其核心本质还是调用epoll_create

图片

这个函数创建了一个 epoll 实例,并为事件数组分配了内存。epfdepoll 文件描述符,用于之后的所有 epoll 操作。

注册事件:

图片

aeApiAddEvent 用于向 epoll 中注册事件,aeApiDelEvent 用于从 epoll 中删除事件其本质依然调用是epoll_ctl。

事件循环:

Redis 的事件循环在 ae.c 中定义,如下所示:

图片

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

在这个循环中,Redis 检查是否有任何文件事件或时间事件需要处理。如果有,它会相应地调用处理函数。

处理 epoll`事件:

ae_epoll.c 中,Redis 定义了如何使用 epoll 来等待和接收事件。其中重点是调用epoll_wait

图片

epoll_wait 函数等待事件发生,并填充 state->events 数组。然后,Redis 遍历这些事件,并根据它们的类型设置相应的掩码(例如 AE_READABLEAE_WRITABLE),然后将事件添加到 eventLoop->fired 数组中,以便进一步处理。

处理文件事件:

在事件循环中,当 epoll_wait 返回就绪的文件描述符时,Redis 会调用 aeProcessEvents 函数来处理这些事件。

    /* ... */
    // 处理文件事件
    for (i = 0; i < numevents; i++) {
        aeFileEvent *fe = &eventLoop->events[eventLoop->fired[i].fd];
        int mask = eventLoop->fired[i].mask;
        int fd = eventLoop->fired[i].fd;
        int rfired = 0;

        /* Note the fe->mask & mask & ... code: maybe an already processed
         * event removed an element that fired and we still didn't
         * processed, so we check if the event is still valid. */
        if (fe->mask & mask & AE_READABLE) {
            rfired = 1;
            fe->rfileProc(eventLoop,fd,fe->clientData,mask);
        }
        if (fe->mask & mask & AE_WRITABLE) {
            if (!rfired || fe->wfileProc != fe->rfileProc)
                fe->wfileProc(eventLoop,fd,fe->clientData,mask);
        }
    }
    /* ... */
}

图片

在这里,Redis 会根据就绪的文件描述符和事件类型调用相应的处理函数,比如 rfileProc 处理读事件,wfileProc 处理写事件。

总结:

Redis 的 I/O 多路复用技术是它能够高效处理大量并发连接的关键。通过抽象出不同的 I/O 多路复用 API,并封装成统一的事件处理接口,Redis 能够在不同的操作系统上运行,并保持高性能。上面的源码解析只是一个简化的概述,实际的 Redis 源码要复杂得多,涉及到更多的细节和异常处理。

本文基于redis 3.2 源码解读。有一定解读差异。

1.epoll_create : 创建一个epoll 句柄
2.epoll_ctl 向内核添加、修改或删除要监听的文件描述符
3.epoll_wait 类似发起select调用