Redis Server跑在单进程单线程中,接收到的命令操作都是按照顺序线性执行的,即便如此,它的读写性能依然能达到10W+的QPS,不得不说:Redis的设计十分优秀。
为什么Redis的读写性能这么高呢?原因有许多,我们列举主要的三个:
1、Redis基于内存操作:
绝大部分的请求为纯粹的内存操作,而且使用hash结构存储数据,查找和操作的时间复杂度均为O(1)。
2、Redis数据结构简单:
redis对数据的操作还是比较简单的,而且redis的数据结构是专门设计的。
3、单线程-IO多路复用模型:
单线程的设计省去了很多的麻烦:比如上下文切换、资源竞争、CPU切换消耗以及各种锁操作等等问题,而IO多路复用模型的使用更让Redis提升了效率。
今天,我们就来聊一聊:IO多路复用模型。
IO即为网络I/O,多路即为多个TCP连接,复用即为共用一个线程或者进程,模型最大的优势是系统开销小,不必创建也不必维护过多的线程或进程。
IO多路复用是经典的Reactor设计模式,有时也称为异步阻塞IO(异步指socket为non-blocking,堵塞指select堵塞),为常见的四种IO模型之一,其他三种分别是:同步堵塞IO、同步非堵塞IO、异步(非堵塞)IO。
IO多路复用的核心是可以同时处理多个连接请求,为此使用了两个系统调用,分别是:
1.select/poll/epoll--模型机制:可以监视多个描述符(fd),一旦某个描述符就绪(读/写/异常)就能通知程序进行相应的读写操作。读写操作都是自己负责的,也即是阻塞的,所以本质上都是同步(堵塞)IO。Redis支持这三种机制,默认使用epoll机制。2.recvfrom--接收数据。而blocking IO只调用了recvfrom,所以在连接数不高的情况下,blocking IO的性能不一定比IO多路复用差。
补充:异步IO无需自己负责读写操作。
1、select机制:
select原理:
当client连接到server后,server会创建一个该连接的描述符fd,fd有三种状态,分别是读、写、异常,存放在三个fd_set(其实就是三个long型数组)中。
#include
//select接口,调用时会堵塞
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
而select()的本质就是通过设置和检查fd的三个fd_set来进行下一步处理。
select大体执行步骤:
1) 使用copy_from_user从用户空间拷贝fd_set到内核空间
2) 内核遍历[0,nfds)范围内的每个fd,调用fd所对应的设备的驱动poll函数,poll函数可以检测fd的可用流(读流、写流、异常流)。
3) 检查是否有流发生,如果有发生,把流设置对应的类别,并执行4,如果没有流发生,执行5。或者timeout=0,执行4。
4) select返回。
5)select阻塞进程,等待被流对应的设备唤醒时执行2,或timeout到期,执行4。
select的局限性:
2、poll机制:
poll原理:
poll的实现和select非常相似,轮询+遍历+根据描述符的状态处理,只是fd_set换成了pollfd,而且去掉了最大描述符数量限制,其他的局限性同样存在。
#include
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生了的事件 */
} ;
3、epoll机制:epoll对select和poll进行了改进,避免了上述三个局限性。
epoll原理:
与select和poll只提供一个接口函数不同的是,epoll提供了三个接口函数及一些结构体:
/*建一个epoll句柄*/
int epoll_create(int size);
/*向epoll句柄中添加需要监听的fd和时间event*/
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);
struct eventpoll
{
...
/*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,也就是这个epoll_ctl监控的事件*/
struct rb_root rbr;
/*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/
struct list_head rdllist;
1.调用epoll_create:linux内核会在epoll文件系统创建一个file节点,同时创建一个eventpoll结构体,结构体中有两个重要的成员:rbr是一棵红黑树,用于存放epoll_ctl注册的socket和事件;rdllist是一条双向链表,用于存放准备就绪的事件供epoll_wait调用。
2.调用epoll_ctl:会检测rbr中是否已经存在节点,有就返回,没有则新增,同时会向内核注册回调函数ep_poll_callback,当有事件中断来临时,调用回调函数向rdllist中插入数据,epoll_ctl也可以增删改事件。
3.调用epoll_wait:返回或者判断rdllist中的数据即可。
epoll两种工作模式:LT--水平触发 ET--边缘触发
LT:只要文件描述符还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作。
ET:检测到有IO事件时,通过epoll_wait调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,必须将该文件描述符一直读到空,让errno返回EAGAIN为止,否则下次的epoll_wait不会返回余下的数据,会丢掉事件。
ET比LT更加高效,因为ET只通知一次,而LT会通知多次,LT可能会充斥大量不关心的就绪文件描述符。
epoll总结: