Redis单线程IO模型

【Redis是单线程】
除Redis之外,Node.js以及Nginx都是单线程,都是服务器高性能的典范。
Redis所有的数据都在内存中,所有的运算都是内存级别的运算。对于复杂度为O(n)级别的指令,一定要谨慎使用,否则可能会因为处理这个指令而导致其它客户端使用Redis卡顿。
那么Redis既然是单线程,它如何高效处理那么多并发客户端的连接呢?
【非阻塞IO】
调用套接字的读写方法时,默认是阻塞的。
read要传递参数n,表示最多读取n个字节后再返回,如果一个字节都没有,线程就会卡在那里,直至新的数据到来或者连接关闭,read方法才可以返回,线程才能继续处理。
write一般不会阻塞,除非内核为套接字分配的写缓冲区已经满了,write方法就会阻塞,直至缓存区有空间空闲出来。
而非阻塞IO在套接字对象上提供了Non_Blocking选项,当这个选项打开时,读写不会阻塞,能读多少取决于内核为套接字分配的读缓冲区内部的数据字节数,能写多少取决于内核为套接字分配的写缓冲区的空闲字节数。读写方法都会通过返回值来告知程序实际读写了多少个字节。
非阻塞IO意味着线程在读写IO时可以不必阻塞,读写可以瞬时完成,而后线程就可以干别的事情了。
【多路复用】
其实上文的阻塞IO和非阻塞IO就是同步和异步的操作,同步操作是等待对方完成,再进行自己这边的逻辑,而异步操作执行完本次操作就干别的去了,所以对于读和写操作,如果没有全部完成,那么剩下的部分可以什么时候再次去读和写,系统应该给线程通知。
事件轮询API就是做这个事情的。
最简单的事件轮询API是select函数,它是操作系统提供给用户程序的API。输入是读写描述符列表read_fds & write_fds,输出是与之对应可读可写事件。同时还提供了一个timeout参数,如果没有任何事件到来,最多等待timeout这么长的时间,线程处于阻塞状态。一旦期间有任何事件到来,就可以立即返回。拿到事件后,线程就可以继续挨个处理相应的事件,处理完了继续过来轮询。这个线程就是一个死循环,这个就是事件循环,一个循环为一个周期。

这里我们可以参考linux的man手册中对于其提供的系统函数select的一些说明:
https://man7.org/linux/man-pages/man2/select.2.html
因为我们通过select系统调用同时处理多个通道描述符的读写事件,因此我们将这类系统调用成为多路复用API。现代操作系统的多路复用API已经改用了epoll和kqueue,在描述符特别多时,后者的性能比select好很多。
事件轮询API就是Java里的NIO技术,很多语言都有这个技术,本质都是一样的。
【指令队列与响应队列】
Redis会将每个客户端套接字都关联一个指令队列,每个客户端的指令通过队列来排队进行顺序处理,先到先服务。
Redis同样也为每个客户端套接字关联一个响应队列,Redis服务器通过响应这一个一个的响应队列来将指令的返回结果返回给对应的客户端。若队列为空,那么连接就暂时空闲,那么当前的客户端描述符会从write_fds里移除来,避免select系统调用立即返回写事件,导致CPU飙升。
【定时任务】
服务器除了响应IO,还要处理别的事情。例如定时任务。如果线程阻塞在select系统调用上,定时任务将无法得到准确调度。Redis的定时任务会被记录在一个最小堆(马上要执行的放在堆的顶点)的接收中,每个循环周期里,Redis都会对最小堆顶点的任务进行处理,处理完毕后,除了整理这个最小堆使下次任务放在顶点之外,Redis还会记录这个马上要执行的任务是在多长时间之后,然后将这个时间设置为select系统调用的timeout。这样一来,未来的timeout就可以放心的交给对应的select调用而不必担心会错过定时任务。

原文地址:https://www.cnblogs.com/bruceChan0018/p/15700080.html