Redis与Reactor模式

Jan 9, 2016

近期看了Redis的设计与实现,这本书写的还不错,看完后对Redis的理解有非常大的帮助。

另外,作者整理了一份Redis源代码凝视,大家能够clone下来阅读。

Redis是开源的缓存数据库,因为其高性能而受到大家的欢迎。同一时候,它的代码量仅仅有6w多行,相比起mysql动则上百万行的代码量,实现比較简单。

Redis中有非常多方面都非常有意思,在这篇文章中我想探讨的是Redis中的Reactor模式。

文件夹

从Redis的工作模式谈起

我们在使用Redis的时候。一般是多个client连接Redisserver,然后各自发送命令请求(比如Get、Set)到Redisserver,最后Redis处理这些请求返回结果。

那Redis服务端是使用单进程还是多进程,单线程还是多线程来处理client请求的呢?

答案是单进程单线程。

当然。Redis除了处理client的命令请求还有诸如RDB持久化、AOF重写这种事情要做。而在做这些事情的时候,Redis会fork子进程去完毕。但对于acceptclient连接、处理client请求、返回命令结果等等这些。Redis是使用主进程及主线程来完毕的。

我们可能会吃惊Redis在使用单进程及单线程来处理请求为什么会如此高效?在回答这个问题之前,我们先来讨论一个I/O多路复用的模式--Reactor。

Reactor模式

C10K问题

考虑这样一个问题:有10000个client须要连上一个server并保持TCP连接。client会不定时的发送请求给server,server收到请求后需及时处理并返回结果。我们应该怎么解决?

方案一:我们使用一个线程来监听,当一个新的client发起连接时,建立连接并new一个线程来处理这个新连接。

缺点:当client数量非常多时,服务端线程数过多,即便不压垮server,因为CPU有限其性能也极其不理想。

因此此方案不可用。

方案二:我们使用一个线程监听。当一个新的client发起连接时。建立连接并使用线程池处理该连接。

长处:client连接数量不会压垮服务端。

缺点:服务端处理能力受限于线程池的线程数,并且假设client连接中大部分处于空暇状态的话服务端的线程资源被浪费。

因此。一个线程只处理一个client连接不管怎样都是不可接受的。那能不能一个线程处理多个连接呢?该线程轮询每一个连接,假设某个连接有请求则处理请求。没有请求则处理下一个连接,这样能够实现吗?

答案是肯定的。并且不必轮询。

我们能够通过I/O多路复用技术来解决问题。

I/O多路复用技术

现代的UNIX操作系统提供了select/poll/kqueue/epoll这种系统调用,这些系统调用的功能是:你告知我一批套接字。当这些套接字的可读或可写事件发生时,我通知你这些事件信息。

依据圣经《UNIX网络编程卷1》,当例如以下任一情况发生时。会产生套接字的可读事件:

  • 该套接字的接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的大小;
  • 该套接字的读半部关闭(也就是收到了FIN),对这种套接字的读操作将返回0(也就是返回EOF)。
  • 该套接字是一个监听套接字且已完毕的连接数不为0;
  • 该套接字有错误待处理,对这种套接字的读操作将返回-1。

当例如以下任一情况发生时,会产生套接字的可写事件:

  • 该套接字的发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的大小;
  • 该套接字的写半部关闭,继续写会产生SIGPIPE信号;
  • 非堵塞模式下。connect返回之后。该套接字连接成功或失败;
  • 该套接字有错误待处理。对这种套接字的写操作将返回-1。

此外,在UNIX系统上,一切皆文件。

套接字也不例外。每个套接字都有相应的fd(即文件描写叙述符)。我们简单看看这几个系统调用的原型。

select(int nfds, fd_set *r, fd_set *w, fd_set *e, struct timeval *timeout)

对于select(),我们须要传3个集合。r,w和e。当中,r表示我们对哪些fd的可读事件感兴趣,w表示我们对哪些fd的可写事件感兴趣。

每一个集合事实上是一个bitmap,通过0/1表示我们感兴趣的fd。比如,我们对于fd为6的可读事件感兴趣,那么r集合的第6个bit须要被设置为1。

这个系统调用会堵塞,直到我们感兴趣的事件(至少一个)发生。调用返回时。内核相同使用这3个集合来存放fd实际发生的事件信息。

也就是说,调用前这3个集合表示我们感兴趣的事件,调用后这3个集合表示实际发生的事件。

select为最早期的UNIX系统调用。它存在4个问题:1)这3个bitmap有限制大小(FD_SETSIZE,通常为1024);2)因为这3个集合在返回时会被内核改动,因此我们每次调用时都须要又一次设置;3)我们在调用完毕后须要扫描这3个集合才干知道哪些fd的读/写事件发生了,普通情况下全量集合比較大而实际发生读/写事件的fd比較少。效率比較低下。4)内核在每次调用都须要扫描这3个fd集合,然后查看哪些fd的事件实际发生,在读/写比較稀疏的情况下相同存在效率问题。

因为存在这些问题,于是人们对select进行了改进。从而有了poll。

poll(struct pollfd *fds, int nfds, int timeout)

struct pollfd {
int fd;
short events;
short revents;
}

poll调用须要传递的是一个pollfd结构的数组。调用返回时结果信息也存放在这个数组里面。 pollfd的结构中存放着fd、我们对该fd感兴趣的事件(events)以及该fd实际发生的事件(revents)。

poll传递的不是固定大小的bitmap,因此select的问题1攻克了。poll将感兴趣事件和实际发生事件分开了,因此select的问题2也攻克了。但select的问题3和问题4仍然没有解决。

select问题3比較easy解决,仅仅要系统调用返回的是实际发生对应事件的fd集合,我们便不须要扫描全量的fd集合。

对于select的问题4,我们为什么须要每次调用都传递全量的fd呢?内核可不能够在第一次调用的时候记录这些fd,然后我们在以后的调用中不须要再传这些fd呢?

问题的关键在于无状态。

对于每一次系统调用,内核不会记录下不论什么信息。所以每次调用都须要反复传递同样信息。

上帝说要有状态。所以我们有了epoll和kqueue。

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll_create的作用是创建一个context,这个context相当于状态保存者的概念。

epoll_ctl的作用是,当你对一个新的fd的读/写事件感兴趣时,通过该调用将fd与对应的感兴趣事件更新到context中。

epoll_wait的作用是,等待context中fd的事件发生。

就是这么简单。

epoll是Linux中的实现,kqueue则是在FreeBSD的实现。

int kqueue(void);
int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout);

与epoll同样的是,kqueue创建一个context。与epoll不同的是。kqueue用kevent取代了epoll_ctl和epoll_wait。

epoll和kqueue攻克了select存在的问题。通过它们,我们能够高效的通过系统调用来获取多个套接字的读/写事件,从而解决一个线程处理多个连接的问题。

Reactor的定义

通过select/poll/epoll/kqueue这些I/O多路复用函数库。我们攻克了一个线程处理多个连接的问题,但整个Reactor模式的完整框架是如何的呢?參考这篇paper。我们能够对Reactor模式有个完整的描写叙述。

Handles :表示操作系统管理的资源,我们能够理解为fd。

Synchronous Event Demultiplexer :同步事件分离器。堵塞等待Handles中的事件发生。

Initiation Dispatcher :初始分派器,作用为加入Event handler(事件处理器)、删除Event handler以及分派事件给Event handler。

也就是说,Synchronous Event Demultiplexer负责等待新事件发生,事件发生时通知Initiation Dispatcher,然后Initiation Dispatcher调用event handler处理事件。

Event Handler :事件处理器的接口

Concrete Event Handler :事件处理器的实际实现,并且绑定了一个Handle。由于在实际情况中,我们往往不止一种事件处理器,因此这里将事件处理器接口和实现分开,与C++、Java这些高级语言中的多态类似。

以上各子模块间协作的步骤描写叙述例如以下:

  1. 我们注冊Concrete Event Handler到Initiation Dispatcher中。

  2. Initiation Dispatcher调用每一个Event Handler的get_handle接口获取其绑定的Handle。

  3. Initiation Dispatcher调用handle_events開始事件处理循环。在这里,Initiation Dispatcher会将步骤2获取的全部Handle都收集起来,使用Synchronous Event Demultiplexer来等待这些Handle的事件发生。

  4. 当某个(或某几个)Handle的事件发生时,Synchronous Event Demultiplexer通知Initiation Dispatcher。

  5. Initiation Dispatcher依据发生事件的Handle找出所相应的Handler。

  6. Initiation Dispatcher调用Handler的handle_event方法处理事件。

时序图例如以下:

另外,该文章举了一个分布式日志处理的样例,感兴趣的同学能够看下。

通过以上的叙述,我们清楚了Reactor的大概框架以及涉及到的底层I/O多路复用技术。

Java中的NIO与Netty

谈到Reactor模式。在这里奉上Java大神Doug Lea的Scalable IO in Java,里面提到了Java网络编程中的经典模式、NIO以及Reactor,而且有相关代码帮助理解。看完后获益良多。

另外。Java的NIO是比較底层的,我们实际在网络编程中还须要自己处理非常多问题(譬如socket的读半包),稍不注意就会掉进坑里。幸好,我们有了Netty这么一个网络处理框架。免去了非常多麻烦。

Redis与Reactor

在上面的讨论中,我们了解了Reactor模式,那么Redis中又是怎么使用Reactor模式的呢?

首先。Redisserver中有两类事件,文件事件和时间事件。

  • 文件事件(file event):Redisclient通过socket与Redisserver连接,而文件事件就是server对套接字操作的抽象。

    比如,client发了一个GET命令请求。对于Redisserver来说就是一个文件事件。

  • 时间事件(time event):server定时或周期性运行的事件。比如,定期运行RDB持久化。

在这里我们主要关注Redis处理文件事件的模型。

參考《Redis的设计与实现》,Redis的文件事件处理模型是这种:

在这个模型中,Redisserver用主线程运行I/O多路复用程序、文件事件分派器以及事件处理器。并且。虽然多个文件事件可能会并发出现。Redisserver是顺序处理各个文件事件的。

Redisserver主线程的运行流程在Redis.c的main函数中体现。而关于处理文件事件的基本的有这几行:

int main(int argc, char **argv) {
...
initServer();
...
aeMain();
...
aeDeleteEventLoop(server.el);
return 0;
}

在initServer()中,建立各个事件处理器。在aeMain()中。运行事件处理循环。在aeDeleteEventLoop(server.el)中关闭停止事件处理循环;最后退出。

总结

在这篇文章中,我们从Redis的工作模型開始,讨论了C10K问题、I/O多路复用技术、Java的NIO。最后回归到Redis的Reactor模式中。

如有纰漏,恳请大家指出,我会一一加以勘正。谢谢!

Redis与Reactor模式的更多相关文章

  1. 理解Redis的反应堆模式

    1. Redis的网络模型 Redis基于Reactor模式(反应堆模式)开发了自己的网络模型,形成了一个完备的基于IO复用的事件驱动服务器,但是不由得浮现几个问题: 为什么要使用Reactor模式呢 ...

  2. 知识联结梳理 : I/O多路复用、EPOLL(SELECT/POLL)、NIO、Event-driven、Reactor模式

    为了形成一个完整清晰的认识,将概念和关系梳理出来,把坑填平. I/O多路复用 I/O多路复用主要解决传统I/O单线程阻塞的问题.它通过单线程管理多个FD,当监听的FD有状态变化的时候的,调用回调函数, ...

  3. 高性能IO之Reactor模式(转载)

    讲到高性能IO绕不开Reactor模式,它是大多数IO相关组件如Netty.Redis在使用的IO模式,为什么需要这种模式,它是如何设计来解决高性能并发的呢? 最最原始的网络编程思路就是服务器用一个w ...

  4. Java IO的Reactor模式

    1.    Reactor出现的原因 Reator模式是大多数IO相关组件如Netty.Redis在使用时的IO模式,为什么需要这种模式,如何设计来解决高性能并发的呢? 最最原始的网络编程思路就是服务 ...

  5. EDA风格与Reactor模式

    本文将探讨如下几个问题: Event-Driven架构风格的约束 EDA风格对架构属性的影响 Reactor架构模式 Reactor所解决的问题 redis中的EventDriven 从观察者模式到E ...

  6. [转帖]Reactor模式

    Reactor模式 https://www.cnblogs.com/crazymakercircle/p/9833847.html 看不懂代码 只看的图.. 疯狂创客圈,一个Java 高并发研习社群  ...

  7. (五:NIO系列) Reactor模式

    出处:Reactor模式 本文目录 1. 为什么是Reactor模式 2. Reactor模式简介 3. 多线程IO的致命缺陷 4. 单线程Reactor模型 4.1. 什么是单线程Reactor呢? ...

  8. 网络IO模型与Reactor模式

    一.三种网络IO模型: 分类: BIO 同步的.阻塞式 IO NIO 同步的.非阻塞式 IO AIO 异步非阻塞式 IO 阻塞和同步的概念: 阻塞:若读写未完成,调用读写的线程一直等待 非阻塞:若读写 ...

  9. Reactor 模式的简单实现

    Reactor 模式简单实现 在网上有部分文章在描述Netty时,会提到Reactor.这个Reactor到底是什么呢?为了搞清楚Reactor到底是什么鬼,我写了一个简单的Demo,来帮助大家理解他 ...

随机推荐

  1. 报告撰写,linux使用gimp简单做gif动图

    我想把我的系统菜单完整记录下来,方便查看,如果单纯使用文字比较单调,使用屏幕截图,需要依次打开多个图像查看也不是很方便,就想到了使用动画的形式展示.由于本人的系统一直使用Linux系统,为了一张gif ...

  2. python3使用urllib获取set-cookies

    #!/usr/bin/env python # encoding: utf-8 import urllib.request from collections import defaultdict re ...

  3. hdu 5102(巧妙的搜索)

    The K-th Distance Time Limit: 8000/4000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others ...

  4. 去除整站下载文件中的tppabs等冗余代码

    用TeleprotUltra复制了一个网站,结果网页中出现了很多形如tppabs=””的冗余代码,点击vs中的“在文件中查找”图标,打开“查找和替换”对话框,转到“快速替换”,然后进行以下设置: “查 ...

  5. Go的50坑:新Golang开发者要注意的陷阱、技巧和常见错误[2]

    初级篇 开大括号不能放在单独的一行 未使用的变量 未使用的Imports 简式的变量声明仅可以在函数内部使用 使用简式声明重复声明变量 偶然的变量隐藏Accidental Variable Shado ...

  6. Lct浅谈

    Lct浅谈 1.对lct的认识 ​ 首先要知道$lct$是什么.$lct$的全称为$link-cut-tree$.通过全称可以看出,这个数据结构是维护树上的问题,并且是可以支持连边断边操作.$lct$ ...

  7. PowerDisginer中NAME与COMMENT转换脚本

    Option Explicit ValidationMode = True InteractiveMode = im_Batch Dim mdl ' the current model ' get t ...

  8. 【转载】uboot的工具mkimage使用方法

    uboot源代码的tools/目录下有mkimage工具,这个工具可以用来制作不压缩或者压缩的多种可启动映象文件. mkimage在制作映象文件的时候,是在原来的可执行映象文件的前面加上一个0x40字 ...

  9. 【微信】微信小程序 调用this.setData报错this.setData is not a function;

    在调用方法过程中 报错如下: 代码如下: Page({ /** * 页面的初始数据 */ data: { userLocalInfo:'用户地址' }, /** * 返回swapping页面 */ b ...

  10. eclipse和maven生成web项目的war包的操作方法

    一.eclipse中,在需要打包的项目名上右击,然后把鼠标光标指向弹出框中的“run as”: 二.之后会看到在这个弹出框的右侧会出现一个悬浮窗,如下: 三.在上边的第二个悬浮窗鼠标点击“maven  ...