Redis线程IO模型的秘密知多少
在前面事务里面讲过Redis是一个单线程应用程序,当然我们比较有代表性的单线程还有Node.js、Nginx等。
那么既然是单线程的为什么还这么快呢?
Redis的数据都在内存里面,所有的运算都是内存级别,处理数据是非常快速的,所以这里得注意一些复杂度为O(n)的指令,可能会导致服务器卡顿。
那么Redis是一个单线程是如何处理并发客户端的连接呢?
这就是接下来要讲的非阻塞IO、多路复用和事件轮询API。
那什么是阻塞IO模型?即在读写数据过程中会发生阻塞现象。
当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。
####阻塞IO模型,如果数据没有就绪,就会一直阻塞在read方法。
data = socket.read();
非阻塞IO
当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果,不管你有没有发送进来,会立即执行下一行代码。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
最简单的事件轮询API是select函数,它是操作系统提供给用户程序的API。输入是读写描述符列表read_fds&write_fds,输出是与之对应的可读可写事件。同时还提供了一个timeout参数,如果没有任何事件到来,那么就最多等待timeout的值的时间,线程处于阻塞状态。一旦期间有任何事件到来,就可以立即返回。时间过了之后还是没有任何事件到来,也会主即返回 。
因为我们通过select系统调用同时处理多个通道描述待的读写事件,因此我们将这类系统调用称为多路复用API。现代操作系统的多路复用API已经不再使用select系统调用,而改用epoll(linux)和kqueue(FreeBSD)和(macosx),因为select系统调用的性能在描述符特别多时会变得非常差。它们使用起来可能在形式上略有差异,但是本质上都是差不多的,都可以使用上面的伪代码逻辑进行理解。
Redis会将每个客户端套接字都关联一个指令队列。客户端的指令通过队列来排队进行顺序处理,先到先服务。
Redis同样也会为每个客户端套接字关联一个晌应队列。Redis服务器通过响应队列来将指令的返回结果回复给客户端。
如果队列为空,那么意昧着连接暂时处于空闲状态,不需要去获取写事件,也就是可以将当前的客户端描述符从write_fds里面移出来。等到队列有数据了,再将描述符放进去,避免select系统调用立即返回写事件,结果发现没什么数据可以写,出现这种情况的线程会令CPU消耗飘升。
服务器除了要响应IO事件外,还要处理其他事情。比如定时任务就是非常重要的一件事。如果线程阻塞在select系统调用上,定时任务将无法得到准时调度。那Redis是如何解决这个问题的呢?
Redis的定时任务会记录在一个被称为“最小堆”的数据结构中。在这个堆中,
最快要执行的任务排在堆的最上方。在每个循环周期里,Redis都会对最小堆里面已经到时间点的任务进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是select系统调用的timeout参数。因为Redis知道未来timeout的值的时间内,没有其他定时任务需要处理,所以可以安心睡眠 timeout 的值的时间。
Nginx和Node的事件处理原理和Redis也是类似的。
一名正在抢救的coder
笔名:mangolove