Redis是一个单线程程序
非阻塞IO
当我们调用套接字的读写方法,默认他们是阻塞的,比如read方法要传递一个参数n,表示最多读取这门多字节后返回,如果一个字节都没有,那么线程就会卡在那里,直到新的数据到来或者连接关闭,read方法才可以返回,线程才能够继续处理。而write方法一般来说不会阻塞,除非内核为套接字分配的写缓冲区已经写满了,write方法才会阻塞,直到缓冲区有空闲空间。
非阻塞IO在套接字对象上提供了一个选项Non_Blocking,当这个选项打开时,读写方法不会阻塞。能读多少取决于内核为套接字分配的读缓冲区的空闲空间字节数,能写多少取决于内核为套接字分配的写缓冲区的空闲空间字节数。读方法和写方法都会通过返回值来告知程序实际读写了多少字节。
事件轮询(多路复用)
非阻塞IO有个问题,就是线程要读数据,但是读了一部分就返回了,线程如何知道什么时候应该继续读,也就是当数据到来时,线程如何得到通知。写也是一样。
事件轮询API就是用来解决这个问题的,最简单的时间轮询API是select函数,它是操作系统提供给用户程序的API。输入时读写描述符列表read_fds & write_fds,输出是与之对应的可读可写时间。同时还提供了一个timeout参数,如果没有任何事件的到来,那么最多等待timeout时间,线程处于阻塞状态。一旦期间有事件到来,就可以立即返回。线程就可以继续挨个处理相应的事件。处理完了继续过来轮询。于是线程就进入了一个死循环。我们把这个死循环称为事件轮询,一个循环为一个周期。
事件轮询API就是Java语言中的NIO技术
指令队列
Redis会将每个客户端套接字都关联一个指令队列。客户端的指令通过队列来排队进行顺序处理,先到先服务。
响应队列
Redis同样会为每个客户端套接字关联一个响应队列。
Redis服务器通过响应队列来将指令的返回结果回复给客户端。如果队列为空,那么以为这连接暂时处于空闲状态,不需要去获取写事件,也就是可以将当前的客户端描述符从write_fds里面移除。等到队列有数据了,再将描述符放进去。避免select系统调用立即返回写事件,结果发现没什么数据可以写。出现这种情况的线程会飙高CPU
定时任务
服务器处理要响应的IO事件外,还要处理其他事情。比如定时任务。
如果线程阻塞在select系统调用上,定时任务将无法得到准时调度。那么Redis是如何解决这个问题的呢?
Redis的定时任务会记录在一个最小堆的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期,Redis都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是select系统调用的timeout参数。因为Redis知道未来timeout时间内,没有其他定时任务需要处理,所以可以安心睡眠timeout时间。