从Redis的工作模式谈起
我们在使用Redis的时候,通常是多个客户端连接Redis服务器,然后各自发送命令请求(例如Get、Set)到Redis服务器,最后Redis处理这些请求返回结果。
那Redis服务端是使用单进程还是多进程,单线程还是多线程来处理客户端请求的呢?
答案是单进程单线程。
当然,Redis除了处理客户端的命令请求还有诸如RDB持久化、AOF重写这样的事情要做,而在做这些事情的时候,Redis会fork子进程去完成。但对于accept客户端连接、处理客户端请求、返回命令结果等等这些,Redis是使用主进程及主线程来完成的。我们可能会惊讶Redis在使用单进程及单线程来处理请求为什么会如此高效?在回答这个问题之前,我们先来讨论一个I/O多路复用的模式--Reactor。
Reactor模式
C10K问题
考虑这样一个问题:有10000个客户端需要连上一个服务器并保持TCP连接,客户端会不定时的发送请求给服务器,服务器收到请求后需及时处理并返回结果。我们应该怎么解决?
方案一:我们使用一个线程来监听,当一个新的客户端发起连接时,建立连接并new一个线程来处理这个新连接。
缺点:当客户端数量很多时,服务端线程数过多,即便不压垮服务器,由于CPU有限其性能也极其不理想。因此此方案不可用。
方案二:我们使用一个线程监听,当一个新的客户端发起连接时,建立连接并使用线程池处理该连接。
优点:客户端连接数量不会压垮服务端。
缺点:服务端处理能力受限于线程池的线程数,而且如果客户端连接中大部分处于空闲状态的话服务端的线程资源被浪费。
因此,一个线程仅仅处理一个客户端连接无论如何都是不可接受的。那能不能一个线程处理多个连接呢?该线程轮询每个连接,如果某个连接有请求则处理请求,没有请求则处理下一个连接,这样可以实现吗?
答案是肯定的,而且不必轮询。我们可以通过I/O多路复用技术来解决这个问题。
I/O多路复用技术
现代的UNIX操作系统提供了select/poll/kqueue/epoll这样的系统调用,这些系统调用的功能是:你告知我一批套接字,当这些套接字的可读或可写事件发生时,我通知你这些事件信息。
根据圣经《UNIX网络编程卷1》,当如下任一情况发生时,会产生套接字的可读事件:
该套接字的接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的大小;
该套接字的读半部关闭(也就是收到了FIN),对这样的套接字的读操作将返回0(也就是返回EOF);
该套接字是一个监听套接字且已完成的连接数不为0;
该套接字有错误待处理,对这样的套接字的读操作将返回-1。
当如下任一情况发生时,会产生套接字的可写事件:
该套接字的发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的大小;
该套接字的写半部关闭,继续写会产生SIGPIPE信号;
非阻塞模式下,connect返回之后,