深入透析Redis单线程Reactor模型

1,067 阅读5分钟

Redis是单线程,基于Reactor模型设计的高效内存数据库,它的核心其实是事件驱动组成,我们常说的Reactor模型就在事件驱动模型中有体现,如下 Redis是一个事件驱动程序,有以下两类事件

  • 文件事件:Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,文件事件是服务器对套接字操作的抽象,服务器与客户端的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
  • 时间事件:Redis中的一些定时操作(比如serverCron函数)需要在给定时间点执行,时间事件就是这类定时操作的抽象

文件事件

redis的网络事件处理器是基于Reactor模式,又叫做文件事件处理器。

  • 文件事件处理器使用I/O多路复用来同时监听多个套接字,并根据套接字执行的任务关联到不同的事件处理器

文件事件以单线程方式运行,但通过使用I/O多路复用程序来监听多个套接字,文件事件处理器实现了高性能的网络通信模型。

文件事件处理起构成

文件事件处理器的四个组成部分,它们分别是套接字、I/O多路复用程序、文件事件分派器(dispatcher),以及事件处理器如下图

image-20210627222927238.png

套接字:文件事件是对套接字操作的抽象,每当一个套接字准备好执行连接应答(accept)、写入、读取、关闭等操作时,就会产生一个文件事件。因为一个服务器通常会连接多个套接字,所以多个文件事件有可能会并发地出现。

I/O多路复:I/O多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字。I/O多路复用将所有事件的套接字都放入一个队列中,该队列就保证有序同步的方式将套接字向分派器传送套接字。

image-20210627223437647.png

文件事件分派器:接收I/O多路复用程序传来的套接字,并根据套接字产生的事件的类型,调用相应的事件处理器。

事件处理器:服务器会为执行不同任务的套接字关联不同的事件处理器,这些处理器是一个个函数,它们定义了某个事件发生时,服务器应该执行的动作。

I/O多路复用这儿要注意,Redis线程安全仅仅对于socket套接字建立的顺序而言。比如两个客户端c1,c2,c1写入k1="a",c2也同时写入k1="b",然后c2读取k1;此时读取的k1就具有不确定性,因为c2的两次操作不是事务性,且网络是存在波动的。但想解决这种问题也很简单,客户端c2将两次操作写入Lua脚本中,就能实现事务的原子性。

综合来说Redis就是单线程的Reactor模型,那么什么是Reactor模型,Reactor模型是管理套接字的一种模式,所以想要透彻理解旧的从I/O线程模型说起。

I/O线程模型--Reactor模式

最经典的多线程非阻塞I/O模型方式是Reactor模式。首先看单线程下的Reactor,当多个客户端向服务器请求时,服务器端会保存一个套接字连接列表中,Reactor将服务器端的整个处理过程分成若干个事件,例如分为接收事件、读事件、写事件、执行事件等。Reactor通过事件检测机制将这些事件分发给不同处理器去处理。接下来介绍套接字的遍历和内核的回调。

内核遍历套接字的事件检测

内核中的套接字都对应一个回调函数,当客户端往套接字发送数据时,内核从网卡接收数据后就会调用回调函数,在回调函数中维护事件列表,应用层获取此事件列表即可得到所有感兴趣的事件。

服务器端有多个客户端套接字连接。首先,应用层告诉内核每个套接字感兴趣的事件。接着,当客户端发送数据过来时,对应会有一个回调函数(类似epoll、select、kqueue),内核从网卡复制数据成功后即调回调函数将套接字1作为可读事件event1加入到事件列表。同样地,内核发现网卡可写时就将套接字2作为可写事件event2添加到事件列表中。最后,应用层向内核请求读、写事件列表,内核将包含了event1和event2的事件列表返回应用层,应用层通过遍历事件列表得知套接字1有数据待读取,于是进行读操作,而套接字2则可以写入数据。如图1.15所示

image-20210628095657493.png

I/O多路复用Redis基于内核的实现

Redis的I/O多路复用程序都是通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数库来实现的。可以在Redis源码中看到对应的源文件,例如ae_select.c、ae_epoll.c、ae_kqueue.c。至于选择哪个函数,程序会在编译时自动选择性能最高的I/O多路复用函数库作为底层实现。