为什么单线程的Redis却能支撑高并发?

应用开发2025-11-03 20:31:388

最近在看 UNIX 网络编程并研究了一下 Redis 的单线实现,感觉 Redis 的程的撑高源代码十分适合阅读和分析,其中 I/O 多路复用(mutiplexing)部分的单线实现非常干净和优雅,在这里想对这部分的程的撑高内容进行简单的整理。

几种 I/O 模型

为什么 Redis 中要使用 I/O 多路复用这种技术呢?单线首先,Redis 是程的撑高跑在单线程中的,所有的单线操作都是按照顺序线性执行的。

但是程的撑高由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回。单线

这会导致某一文件的程的撑高 I/O 阻塞导致整个进程无法对其他客户提供服务,而 I/O 多路复用就是单线为了解决这个问题而出现的。

Blocking I/O

先来看一下传统的程的免费信息发布网撑高阻塞 I/O 模型到底是如何工作的:当使用 Read 或者 Write 对某一个文件描述符(File Descriptor 以下简称 FD)进行读写时。

如果当前 FD 不可读或不可写,单线整个 Redis 服务就不会对其他的程的撑高操作作出响应,导致整个服务不可用。单线

这也就是传统意义上的,我们在编程中使用最多的阻塞模型:

 

阻塞模型虽然开发中非常常见也非常易于理解,但是由于它会影响其他 FD 对应的服务,所以在需要处理多个客户端任务的时候,往往都不会使用阻塞模型。

I/O 多路复用

虽然还有很多其他的 I/O 模型,但是在这里都不会具体介绍。阻塞式的 I/O 模型并不能满足这里的需求,我们需要一种效率更高的 I/O 模型来支撑 Redis 的多个客户(redis-cli)。

这里涉及的就是 I/O 多路复用模型了:

 

在 I/O 多路复用模型中,最重要的服务器租用函数调用就是 select,该方法的能够同时监控多个文件描述符的可读可写情况,当其中的某些文件描述符可读或者可写时,select 方法就会返回可读以及可写的文件描述符个数。

关于 select 的具体使用方法,在网络上资料很多,这里就不过多展开介绍了;

与此同时也有其它的 I/O 多路复用函数 epoll/kqueue/evport,它们相比 select 性能更优秀,同时也能支撑更多的服务。

Reactor 设计模式

Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符)

 

文件事件处理器使用 I/O 多路复用模块同时监听多个 FD,当 accept、read、write 和 close 文件事件产生时,文件事件处理器就会回调 FD 绑定的事件处理器。

虽然整个文件事件处理器是在单线程上运行的,但是通过 I/O 多路复用模块的引入,实现了同时对多个 FD 读写的监控,提高了网络通信模型的云南idc服务商性能,同时也可以保证整个 Redis 服务实现的简单。

I/O 多路复用模块

I/O 多路复用模块封装了底层的 select、epoll、avport 以及 kqueue 这些 I/O 多路复用函数,为上层提供了相同的接口。

 

在这里我们简单介绍 Redis 是如何包装 select 和 epoll 的,简要了解该模块的功能,整个 I/O 多路复用模块抹平了不同平台上 I/O 多路复用函数的差异性,提供了相同的接口:

static int  aeApiCreate(aeEventLoop *eventLoop) static int  aeApiResize(aeEventLoop *eventLoop, int setsize) static void aeApiFree(aeEventLoop *eventLoop) static int  aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask)  static int  aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)

同时,因为各个函数所需要的参数不同,我们在每一个子模块内部通过一个 aeApiState 来存储需要的上下文信息:

// select typedef struct aeApiState {     fd_set rfds, wfds;     fd_set _rfds, _wfds; } aeApiState; // epoll typedef struct aeApiState {     int epfd;     struct epoll_event *events; } aeApiState;

这些上下文信息会存储在 eventLoop 的 void *state 中,不会暴露到上层,只在当前子模块中使用。

封装 Select 函数

Select 可以监控 FD 的可读、可写以及出现错误的情况。在介绍 I/O 多路复用模块如何对 Select 函数封装之前,先来看一下 Select 函数使用的大致流程:

初始化一个可读的 fd_set 集合,保存需要监控可读性的 FD。 使用 FD_SET 将 fd 加入 RFDS。 调用 Select 方法监控 RFDS 中的 FD 是否可读。 当 Select 返回时,检查 FD 的状态并完成对应的操作。 int fd = /* file descriptor */ fd_set rfds; FD_ZERO(&rfds); FD_SET(fd, &rfds) for ( ; ; ) {     select(fd+1, &rfds, NULL, NULL, NULL);     if (FD_ISSET(fd, &rfds)) {         /* file descriptor `fd` becomes readable */     } }

而在 Redis 的 ae_select 文件中代码的组织顺序也是差不多的,首先在 aeApiCreate 函数中初始化 rfds 和 wfds:

static int aeApiCreate(aeEventLoop *eventLoop) {     aeApiState *state = zmalloc(sizeof(aeApiState));     if (!state) return -1;     FD_ZERO(&state->rfds);     FD_ZERO(&state->wfds);     eventLoop->apidata = state;     return 0; }

而 aeApiAddEvent 和 aeApiDelEvent 会通过 FD_SET 和 FD_CLR 修改 fd_set 中对应 FD 的标志位:

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {     aeApiState *state = eventLoop->apidata;     if (mask & AE_READABLE) FD_SET(fd,&state->rfds);     if (mask & AE_WRITABLE) FD_SET(fd,&state->wfds);     return 0; }

整个 ae_select 子模块中最重要的函数就是 aeApiPoll,它是实际调用 select 函数的部分,其作用就是在 I/O 多路复用函数返回时,将对应的 FD 加入 aeEventLoop 的 fired 数组中,并返回事件的个数:

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {     aeApiState *state = eventLoop->apidata;     int retval, j, numevents = 0;     memcpy(&state->_rfds,&state->rfds,sizeof(fd_set));     memcpy(&state->_wfds,&state->wfds,sizeof(fd_set));     retval = select(eventLoop->maxfd+1,                 &state->_rfds,&state->_wfds,NULL,tvp);     if (retval > 0) {         for (j = 0; j <= eventLoop->maxfd; j++) {             int mask = 0;             aeFileEvent *fe = &eventLoop->events[j];             if (fe->mask == AE_NONE) continue;             if (fe->mask & AE_READABLE && FD_ISSET(j,&state->_rfds))                 mask |= AE_READABLE;             if (fe->mask & AE_WRITABLE && FD_ISSET(j,&state->_wfds))                 mask |= AE_WRITABLE;             eventLoop->fired[numevents].fd = j;             eventLoop->fired[numevents].mask = mask;             numevents++;         }     }     return numevents; }

封装 Epoll 函数

Redis 对 epoll 的封装其实也是类似的,使用 epoll_create 创建 epoll 中使用的 epfd:

static int aeApiCreate(aeEventLoop *eventLoop) {     aeApiState *state = zmalloc(sizeof(aeApiState));     if (!state) return -1;     state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);     if (!state->events) {         zfree(state);         return -1;     }     state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */     if (state->epfd == -1) {         zfree(state->events);         zfree(state);         return -1;     }     eventLoop->apidata = state;     return 0; }

在 aeApiAddEvent 中使用 epoll_ctl 向 epfd 中添加需要监控的 FD 以及监听的事件:

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {     aeApiState *state = eventLoop->apidata;     struct epoll_event ee = ; /* avoid valgrind warning */     /* If the fd was already monitored for some event, we need a MOD      * operation. Otherwise we need an ADD operation. */     int op = eventLoop->events[fd].mask == AE_NONE ?             EPOLL_CTL_ADD : EPOLL_CTL_MOD;     ee.events = 0;     mask |= eventLoop->events[fd].mask; /* Merge old events */     if (mask & AE_READABLE) ee.events |= EPOLLIN;     if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;     ee.data.fd = fd;     if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;     return 0; }

由于 epoll 相比 select 机制略有不同,在 epoll_wait 函数返回时并不需要遍历所有的 FD 查看读写情况。

在 epoll_wait 函数返回时会提供一个 epoll_event 数组:

typedef union epoll_data {     void    *ptr;     int      fd; /* 文件描述符 */     uint32_t u32;     uint64_t u64; } epoll_data_t; struct epoll_event {     uint32_t     events; /* Epoll 事件 */     epoll_data_t data; };

其中保存了发生的 epoll 事件(EPOLLIN、EPOLLOUT、EPOLLERR 和 EPOLLHUP)以及发生该事件的 FD。

aeApiPoll 函数只需要将 epoll_event 数组中存储的信息加入 eventLoop 的 fired 数组中,将信息传递给上层模块:

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/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;             if (e->events & EPOLLHUP) mask |= AE_WRITABLE;             eventLoop->fired[j].fd = e->data.fd;             eventLoop->fired[j].mask = mask;         }     }     return numevents; }

子模块的选择

因为 Redis 需要在多个平台上运行,同时为了执行的效率与性能,所以会根据编译平台的不同选择不同的 I/O 多路复用函数作为子模块,提供给上层统一的接口。

在 Redis 中,我们通过宏定义的使用,合理的选择不同的子模块:

#ifdef HAVE_EVPORT #include "ae_evport.c" #else     #ifdef HAVE_EPOLL     #include "ae_epoll.c"     #else         #ifdef HAVE_KQUEUE         #include "ae_kqueue.c"         #else         #include "ae_select.c"         #endif     #endif #endif

因为 select 函数是作为 POSIX 标准中的系统调用,在不同版本的操作系统上都会实现,所以将其作为保底方案:

 

Redis 会优先选择时间复杂度为 $O(1)$ 的 I/O 多路复用函数作为底层实现,包括 Solaries 10 中的 evport、Linux 中的 epoll 和 macOS/FreeBSD 中的 kqueue。

上述的这些函数都使用了内核内部的结构,并且能够服务几十万的文件描述符。

但是如果当前编译环境没有上述函数,就会选择 select 作为备选方案,由于其在使用时会扫描全部监听的描述符,所以其时间复杂度较差 O(n)。

并且只能同时服务 1024 个文件描述符,所以一般并不会以 select 作主要方案使用。

总结

Redis 对于 I/O 多路复用模块的设计非常简洁,通过宏保证了 I/O 多路复用模块在不同平台上都有着优异的性能,将不同的 I/O 多路复用函数封装成相同的 API 提供给上层使用。

整个模块使 Redis 能以单进程运行的同时服务成千上万个文件描述符,避免了由于多进程应用的引入导致代码实现复杂度的提升,减少了出错的可能性。

本文地址:http://www.bzve.cn/html/609f64398747.html
版权声明

本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。

热门文章

全站热门

用电脑合成人物剪纸,打造独特的手工艺品(电脑合成剪纸教程,手工艺品新潮流)

最近试用Kali,在使用hydra的时候,发现一直出现 Too many open files ulimit -n 永远都是 1024 最终发现,问题在于修改/etc/security/limits.conf的时候root是要单独配置的,不能像下面这么写: 重启后ulimit -n 发现生效。。。

OpenVPN是一个用于创建虚拟专用网络加密通道的软件包,允许创建的VPN使用公开密钥、数字证书、或者用户名/密码来进行身份验证。OpenVPN能在Solaris、Linux、OpenBSD、FreeBSD、NetBSD、Mac OS X与Windows 2000/XP/Vista/7以及Android和iOS上运行,并包含了许多安全性的功能。配置OpenVPN 2.0的第一步是建立一个PKI(public key infrastructure 公钥基础设施)。PKI包括:服务端和每个客户端都有一个证书(也称做公钥)和私钥首先,我们必须安装OpenVPN软件。在Ubuntu 15.04和其它带有‘apt’包管理器的Unix系统中,可以通过如下命令安装:复制代码代码如下:注意: 所有接下来的命令要以超级用户权限执行,如在使用sudo -i命令后执行,或者你可以使用sudo -E作为接下来所有命令的前缀。开始之前,我们需要拷贝“easy-rsa”到openvpn文件夹。然后进入到该目录复制代码代码如下:第三,我们需要加载环境变量,这些变量已经在前面一步中编辑好了。生成密钥的最后一步准备工作是清空旧的证书和密钥,以及生成新密钥的序列号和索引文件。可以通过以下命令完成。复制代码代码如下:在对话中,我们可以看到默认的变量,这些变量是我们先前在“vars”中指定的。我们可以检查一下,如有必要进行编辑,然后按回车几次。对话如下Generating a 2048 bit RSA private key复制代码代码如下:该命令的对话如下:Generating a 2048 bit RSA private key复制代码代码如下:该命令的输出样例如下Generating DH parameters, 2048 bit long safe prime, generator 2复制代码代码如下:现在,生成完毕,我们可以移动所有生成的文件到最后的位置中。最后,我们来创建OpenVPN配置文件。让我们从样例中拷贝过来吧:然后编辑我们需要指定密钥的自定义路径一切就绪。在重启OpenVPN后,服务器端配置就完成了。Unix的客户端配置复制代码代码如下:加载环境变量然后创建客户端密钥我们将看到一个与先前关于服务器密钥生成部分的章节描述一样的对话,填入客户端的实际信息。假如需要密码保护密钥,你需要运行另外一个命令,命令如下在此种情况下,在建立VPN连接时,会提示你输入密码。现在,我们需要将以下文件从服务器拷贝到客户端/etc/openvpn/keys/文件夹。服务器文件列表:ca.crt,复制代码代码如下:在此之后,我们需要重启OpenVPN以接受新配置。好了,客户端配置完成。安卓客户端配置复制代码代码如下:所有这些文件我们必须移动我们设备的SD卡上。然后,我们需要安装一个OpenVPN Connect 应用。接下来,配置过程很是简单:打开 OpenVPN 并选择“Import”选项虽然OpenVPN初始配置花费不少时间,但是简易的客户端配置为我们弥补了时间上的损失,也提供了从任何设备连接的能力。此外,OpenVPN提供了一个很高的安全等级,以及从不同地方连接的能力,包括位于NAT后面的客户端。因此,OpenVPN可以同时在家和企业中使用。

NVIDIA 358.16 —— NVIDIA 358 系列的第一个稳定版本已经发布,并对 358.09 中(测试版)做了一些修正,以及一些小的改进。NVIDIA 358 增加了一个新的 nvidia-modeset.ko 内核模块,可以配合 nvidia.ko 内核模块工作来调用 GPU 显示引擎。在以后发布版本中,nvidia-modeset.ko 内核驱动程序将被用于模式设置接口的基础,该接口由内核的直接渲染管理器(DRM)所提供。新的驱动程序也有新的 GLX 协议扩展,以及在 OpenGL 驱动中分配大量内存的系统内存分配新机制。新的 GPU GeForce 805A 和 GeForce GTX 960A 都支持。NVIDIA 358.16 也支持 X.Org 1.18 服务器和 OpenGL 4.3。如何在 Ubuntu 中安装 NVIDIA 358.16 :复制代码代码如下:它会要求你输入密码。输入密码后,密码不会显示在屏幕上,按 Enter 继续。2. 刷新并安装新的驱动程序添加 PPA 后,逐一运行下面的命令刷新软件库并安装新的驱动程序:复制代码代码如下:(假如需要的话,) 卸载:复制代码代码如下:删除所有的 nvidia 包:复制代码代码如下:最后返回菜单并重新启动:复制代码代码如下:要禁用/删除显卡驱动 PPA,点击系统设置下的软件和更新,然后导航到其他软件标签。

电脑显示程序出现未知错误,如何解决?(故障排除与解决方案)

ubuntu安装了wine qq怎么去卸载呢?下面我们分别来演示如何卸载它们1、安装wine按ctrl+alter+T打开终端输入以下两条命令sudo apt-get updatesodo apt-get install wine安装时间有点长,请耐心的等候2、按钮选择期间有个软件包的配置图像界面,需要用户使用tab键选定ok然后下一个条出另一个框,这里移动左右键盘,选择YES。按下enter键进行安装、、、、、3、安装wine-qq下载wine-qq的网址:http://www.longene.org/download/sudo dpkg -i WineQQ2013-xxx(你下载的QQ web包)4、卸载wine-qqsudo dpkg --purge wine-qq2012-longeneteam

Linux发行版本之一Ubuntu 14.10幸运地赶上了Linux内核更新,新内核版本号为3.16.4。根据Ubuntu开发人员的邮件显示,10月9日是14.10内核的冻结日期,那就意味着Linux内核3.16.4将是Ubuntu 14.10的最终核心。毕竟内核更新几乎没有什么规律可言,而且内核需要完成大量的测试后才可以推出。Ubuntu是Linux发行版本之一,使用范围很广泛,一直保持着每6个月一次的更新频率,最新的Ubuntu 14.10 Utopic Unicorn将于10月23日推出。采用新版内核的Ubuntu 14.10 值得期待。谢谢阅读,希望能帮到大家,请继续关注脚本之家,我们会努力分享更多优秀的文章。

Ubuntu 14.04 LTS 已经出来了,我要如何(怎样)升级到Ubuntu 14.04 LTS版本呢?我们可以从镜像或者主要发型版本来升级到最新版本复制代码代码如下:$ uname -mrs复制代码代码如下:Linux 3.2.0-51-generic x86_64复制代码代码如下:$ sudo apt-get update复制代码代码如下:$ sudo do-release-upgratedo-release-upgrate 会运行升级工具。你只需要根据屏幕上的提示操作即可。复制代码代码如下:Checking for a new Ubuntu release复制代码代码如下:sudo do-release-upgrade -d提醒:关于从Ubuntu 13.10 从桌面 升级系统的操作首先,你需要移除所有第三方的二进制驱动,比如 NVIDIA 或者 AMD 显卡驱动。一旦移除后再重启桌面,按住 ALT+F2 并且在 命令框中输入 update-managerupdate manager 会打开并告诉你: New distribution release 14.04 LTS is available(新版的版本 14.04 LTS已经可以使用).只要点击 Upgrade(升级),然后跟着屏幕上的指示操作即可。注意所有的TLS 桌面版用户需要等到一个叫做 Ubuntu LTS v14.04.1 释放出来才行。假如不想等这个版本,可以在 update-manager中使用 -d 参数来升级。可以通过这种方式,将 Ubuntu 12.04 LTSs 升级到 Ubuntu 14.04 LTS 版本:复制代码代码如下:$ sudo reboot然后确认你是否升级到了最新版本;复制代码代码如下:$ lsb_release -a$ uname -mrs$ tail -f /var/log/app/log/file确认升级到最新版本后,再重新安装第三方的二进制驱动。

热门文章

友情链接

滇ICP备2023006006号-39