最近略有闲暇时间,因而对Redis进行了一些学习,学习途径除了官方文档还有Redis源代码,我看的版本是2.8.13,Redis源码总行数不到5W行,不一样组件拆分很是细致,阅读起来也很清晰。这篇博客主要介绍我对Redis网络层架构以及线程模型的一些了解,但愿能对你们有所帮助。linux
网络编程离不开Socket,网络I/O模型最经常使用的无非是同步阻塞、同步非阻塞、异步阻塞、异步非阻塞,高性能网络服务器最多见的线程模型也就是基于EventLoop模式的单线程模型。咱们看看Redis的网络架构是怎么样的:编程
Redis基础组建结构api
这里解释下上图涉及的组件,Redis网络层基础组件主要包括四个部分:数组
要理解Redis的单线程模型,咱们先抛出一些问题,当咱们有多个客户端同时去跟Redis Server创建链接,以后又同时对某个key进行操做,这个过程当中发生了什么呢?会不会有并发问题?服务器
好了,这些问题先丢在这了,咱们看看Redis启动初始化的过程当中会作什么事情,这里尽可能省略了与本文无关的部分:网络
对,这里咱们就把Redis的启动部分简化为三步,跟网络操做有关的主要在第二步和第三步里面,来看看initServer里面发生了什么:架构
initServer流程并发
initServer里面首先建立了一个EventLoop,而后监听Server的IP对应的端口号,假设咱们监听的是127.0.0.1:3333这个IP:端口对,咱们获得的一个Server Socket句柄,最后经过createFileEvent将咱们获得的Server Socket句柄和咱们关心的网络事件mask注册到EventLoop上面。EventLoop是什么呢,咱们看看它的定义:异步
1socket 2 3 4 5 6 7 8 9 10 11 12 |
typedef struct aeEventLoop { int maxfd; /* highest file descriptor currently registered */ int setsize; /* max number of file descriptors tracked */ long long timeEventNextId; time_t lastTime; /* Used to detect system clock skew */ aeFileEvent *events; /* Registered events */ aeFiredEvent *fired; /* Fired events */ aeTimeEvent *timeEventHead; int stop; void *apidata; /* This is used for polling API specific data */ aeBeforeSleepProc *beforesleep; } aeEventLoop; |
上面咱们关注的主要是两个东西:events和fired。他们分别是两个数组,events用于存放被注册的事件以及相应的句柄,fired用于存放当EventLoop线程从多路复用器轮训到有事件的句柄的时候,EventLoop线程会把它放入fired数组里面,而后处理。
事件注册示意图
我用上面的示意图描述createFileEvent作的事情,就是将Server Socket句柄和关心的事件mask以及当事件产生的时候的事件处理器accptHandler生成一个aeFileEvent注册到EventLoop的events的数组里面,固然在这以前会首先将事件注册到多路复用器上,也就是epoll、kqueue等这些组件上。事件注册完以后须要对多路复用器进行轮训,来分离咱们关心切发生的事件,那就是最后一步,启动事件轮询器。
上面的步骤完成了服务端的网络初始化,并且事件轮询器已经开始工做了,事件轮询器作什么事情呢,就是不断轮训多路复用器,看看以前注册的事件有没有发生,若是有发生,则将会将事件分离出来,放入EventLoop的fired数组中,而后处理这些事件。
很显然,上面注册的事件是客户端创建链接这个事件,所以当有两个客户端同时链接Redis服务器的时候,事件轮询器会从多路复用器上面分离出这个事件,同时调用acceptHandler来处理。acceptHandler作的事情主要是accept客户端的链接,建立socket句柄,而后将socket句柄和读事件注册到EventLoop的events数组里面,不同的是对于客户端的事件处理器是readQueryClient。
accept客户端链接以及注册客户端链接句柄示意图
上面示意图表示了acceptHandler处理客户端链接,获得句柄以后再将这个句柄注册到多路复用器以及EventLoop上的示意图。以后再一样再处理下一个客户端的链接,这些都是串行的。
上面接收客户端这部分其实都发生在事件轮训的主循环里面:
1 2 3 4 5 6 7 8 |
void aeMain(aeEventLoop *eventLoop) { eventLoop->stop = 0; while (!eventLoop->stop) { if (eventLoop->beforesleep != NULL) eventLoop->beforesleep(eventLoop); aeProcessEvents(eventLoop, AE_ALL_EVENTS); } } |
Redis会不断的轮训多路复用器,将网络事件分离出来,若是是accept事件,则新接收客户端链接并将其注册到多路复用器以及EventLoop中,若是是查询事件,则经过读取客户端的命令进行相应的处理,这一切都是单线程,顺序的执行的,所以不会发生并发问题。
Redis官网对Redis的读写性能测试结果达到10左右,这是很是吸引人的。Redis的单线程的行为主要是对内存的读写,这些操做其实用不了多少时间,所以瓶颈在网络I/O上面,咱们通常提供较好的网络环境就能够提高Redis的吞吐量,好比提升网络带宽,除此以外还能够经过合并命令提交批处理请求来代替单条命令一次次请求从而减小网络开销,提升吞吐量。
《Redis源码》