北京中科白癜风医院怎样 http://ask.bdfyy999.com/RedisServer跑在单进程单线程中,接收到的命令操作都是按照顺序线性执行的,即便如此,它的读写性能依然能达到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多路复用的核心是可以同时处理多个连接请求,为此使用了两个系统调用,分别是:
select/poll/epoll--模型机制:可以监视多个描述符(fd),一旦某个描述符就绪(读/写/异常)就能通知程序进行相应的读写操作。读写操作都是自己负责的,也即是阻塞的,所以本质上都是同步(堵塞)IO。Redis支持这三种机制,默认使用epoll机制。recvfrom--接收数据。而blockingIO只调用了recvfrom,所以在连接数不高的情况下,blockingIO的性能不一定比IO多路复用差。
补充:异步IO无需自己负责读写操作。
我们来看一下IO多路复用模型的三种实现机制:
1、select机制:
select原理:
当client连接到server后,server会创建一个该连接的描述符fd,fd有三种状态,分别是读、写、异常,存放在三个fd_set(其实就是三个long型数组)中。
#includesys/select.h
//select接口,调用时会堵塞
intselect(intnfds,fd_set*readfds,fd_set*writefds,fd_set*exceptfds,structtimeval*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的局限性:
维护一个存放大量描述符的数组:每次调用select()都需要将fd_set从用户态拷贝到内核态,然后由内核遍历fd_set,如果fd_set很大,那么拷贝和遍历的开销会很大,为了减少性能损坏,内核对fd_set的大小做了限制,并通过宏定义控制,无法改变()。单进程监听的描述符数量有限制:单进程能打开的最大连接数。轮询遍历数组,效率低下:select机制只知道有IO发生,但是不知道是哪几个描述符,每次唤醒,都需要遍历一次,复杂度为O(n)。2、poll机制:
poll原理:
poll的实现和select非常相似,轮询+遍历+根据描述符的状态处理,只是fd_set换成了pollfd,而且去掉了最大描述符数量限制,其他的局限性同样存在。
#includepoll.h
intpoll(structpollfd*fds,unsignedintnfds,inttimeout);
structpollfd{
intfd;/*文件描述符*/
shortevents;/*等待的事件*/
shortrevents;/*实际发生了的事件*/
};
3、epoll机制:epoll对select和poll进行了改进,避免了上述三个局限性。
epoll原理:
与select和poll只提供一个接口函数不同的是,epoll提供了三个接口函数及一些结构体:
/*建一个epoll句柄*/
intepoll_create(intsize);
/*向epoll句柄中添加需要监听的fd和时间event*/
intepoll_ctl(intepfd,intop,intfd,structepoll_event*event);
/*返回发生事件的队列*/
intepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout);
structeventpoll
{
...
/*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,也就是这个epoll_ctl监控的事件*/
structrb_rootrbr;
/*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/
structlist_headrdllist;
...
调用epoll_create:linux内核会在epoll文件系统创建一个file节点,同时创建一个eventpoll结构体,结构体中有两个重要的成员:rbr是一棵红黑树,用于存放epoll_ctl注册的socket和事件;rdllist是一条双向链表,用于存放准备就绪的事件供epoll_wait调用。调用epoll_ctl:会检测rbr中是否已经存在节点,有就返回,没有则新增,同时会向内核注册回调函数ep_poll_callback,当有事件中断来临时,调用回调函数向rdllist中插入数据,epoll_ctl也可以增删改事件。调用epoll_wait:返回或者判断rdllist中的数据即可。epoll两种工作模式:LT--水平触发ET--边缘触发
LT:只要文件描述符还有数据可读,每次epoll_wait都会返回它的事件,提醒用户程序去操作。
ET:检测到有IO事件时,通过epoll_wait调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,必须将该文件描述符一直读到空,让errno返回EAGAIN为止,否则下次的epoll_wait不会返回余下的数据,会丢掉事件。
ET比LT更加高效,因为ET只通知一次,而LT会通知多次,LT可能会充斥大量不关心的就绪文件描述符。
epoll总结:
使用红黑树而不是数组存放描述符和事件,增删改查非常高效,轻易可处理大量并发连接。红黑树及双向链表都在内核cache中,避免拷贝开销。采用回调机制,事件的发生只需