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. windows 2012(64位) IIS配置asp程序 500 - 内部服务器错误。您查找的资源存在问题,因而无法显示。

    在网上找了很久,包括常规的设置父路径之类的,一直都不可以,搞了一晚上毫无成就感,第二天早上无意中看到一篇文章,说到点子上了,非常感谢.源地址已经找不到了,我把大概的问题截图说明一下. 方法如下:1.打 ...

  2. Educational Codeforces Round 1D 【DFS求联通块】

    http://blog.csdn.net/snowy_smile/article/details/49924965 D. Igor In the Museum time limit per test ...

  3. springMVC笔记:jsp页面获取后台数据记录列表

    1.读取数据库中的记录List<HashMap<String,String>> attributes; 2.Controller构造Model如下: @RequestMappi ...

  4. BFS+最小生成树+倍增+LCA【bzoj】4242 水壶

    [bzoj4242 水壶] Description JOI君所居住的IOI市以一年四季都十分炎热著称. IOI市是一个被分成纵H*横W块区域的长方形,每个区域都是建筑物.原野.墙壁之一.建筑物的区域有 ...

  5. UVA——442 Matrix Chain Multiplication

    442 Matrix Chain MultiplicationSuppose you have to evaluate an expression like A*B*C*D*E where A,B,C ...

  6. 【hdu1150】【Machine Schedule】二分图最小点覆盖+简单感性证明

    (上不了p站我要死了,侵权度娘背锅) 题目大意 有两台机器A和B以及N个需要运行的任务.每台机器有M种不同的模式,而每个任务都恰好在一台机器上运行.如果它在机器A上运行,则机器A需要设置为模式ai,如 ...

  7. C语言基础之while的使用

    一. 格式: 1: while (条件) 2:   3: { 4:   5: 循环体 6:   7: } 8:   二.运行原理 1.如果一开始条件就不成立,永远不会执行循环体 2.如果条件成立,就会 ...

  8. ANDROID模拟器访问本地WEB应用10.0.2.2

    在一般的Java Web程序开发中,我们通常使用localhost或者127.0.0.1来访问本机的Web服务,但是如果我们在Android模拟器中也采用同样的地址来访问,Android模拟器将无法正 ...

  9. Using ASIHTTPRequest in an iOS project

    1) Add the files Copy the files you need to your project folder, and add them to your Xcode project. ...

  10. 【Linux】CentOS7上rpm命令批量卸载删除模糊rpm包名

    例如,我要删除如下文件名匹配上wine的所有文件