Redis为什么这么快?这个经典的面试题中答案包含以下几点:
-
内存存储:所有的数据都存储在内存中,这使得数据的读写速度非常快
-
数据结构简单直接:Redis 提供了一些高效的数据结构,如字符串、列表、集合、哈希表和有序集合。这些数据结构被直接实现在内存中,操作它们的时间复杂度通常很低
-
单线程模型:Redis 利用单线程模型来处理客户端的请求,避免了多线程的上下文切换和竞争条件,这样可以确保操作的原子性和一致性,同时提高了性能
-
I/O多路复用技术:I/O 多路复用技术来同时监视多个套接字,并在套接字准备好读取或写入时通知 Redis
-
优化的协议:Redis 协议简单而紧凑,减少了网络传输的数据量
持久化策略、高效的发布/订阅和事务支持、可调的数据淘汰策略、优化的底层实现(跳表)、主从复制和分片等等一系列技术手段
今天这边主要介绍其中的I/O多路复用技术及底层代码实现。
Redis 使用 I/O 多路复用技术来同时处理多个网络连接上的 I/O 操作。
I/O 多路复用是一种允许单个线程监视多个文件描述符(在网络编程中通常是套接字)一旦其中一个或多个文件描述符准备好进行 I/O 操作(例如读取或写入),系统就会通知程序。
多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,变成了一次系统调用 + 内核层遍历这些文件描述符
常见的 I/O 多路复用技术:
-
select:最基本的 I/O 多路复用接口。它允许程序监视多个文件描述符,等待一个或多个文件描述符成为“就绪”状态。
-
poll:与 select 类似,但没有文件描述符数量的限制。
-
epoll(仅限 Linux):比 select 和 poll 更高效,没有文件描述符数量的限制,支持“水平触发”和“边缘触发”。
-
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
实例,并为事件数组分配了内存。epfd
是 epoll
文件描述符,用于之后的所有 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_READABLE
或 AE_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调用