Redis 线程IO模型
Redis 线程IO模型
Redis是单线程程序! 这点必须铭记。
Redis单线程为什么还能这么快?
因为它所有的数据都在内存中, 所有的运算都是内存级别的运算。正因为Redis是单线程, 所以要小心使用Redis指令, 对于那些时间复杂度位O(n)级别的指令, 一定要谨慎使用,一不小心就会可能导致Redis卡顿。
Redis单线程如何处理那么多的并发客户端连接?
因为核心是基于非阻塞的 IO 多路复用机制。
非阻塞IO
当我们调用套接字的读写方法, 默认他们是阻塞的, 比如read方法要传递进去一个参数n, 表示读取这么多字节后再返回, 如果没有读够线程就会卡在那里, 直到新的数据到来或者连接关闭, read方法才可以返回, 线程才能继续处理。而write方法一般来说不会阻塞, 除非内核为套接字分配的写缓冲区已经满了, write方法就会阻塞, 直到缓存区中有空闲空间挪出来。
非阻塞IO再套接字对象上提供了一个选项Non_Blocking, 这个选项打开时, 读写方法不会阻塞, 而是能读多少读多少, 能写多少写多少。能读多少取决于内核内套接字分配的读缓冲区内部的数据字节数, 能写多少取决于内核为套接字分配的写缓冲区的空闲空间字节数。 读方法和写方法都会通过返回值来告知程序实际读写了多少字节。
有了非阻带IO意味着线程再读写IO时可以不必再阻塞了, 读写可以瞬间完成然后线程可以继续干别的事了。
事件轮询(多路复用)
非阻塞IO有个问题, 那就是线程要读数据,结果读了一部分旧返回了, 线程如何知道何时才应该继续读。也就是当数据到来时, 线程如何得到通知。写也是一样, 如果缓冲区满了,写不完, 剩下的数据何时才应该继续写, 线程也应该得到通知。
时间轮询API就是用来解决这个问题的, 最简单的时间轮询API是select函数, 它是操作系统提供给用户程序的API。 输入是读写描述符里欸包read_fds&write_fds,输出是与之对应的可读可写时间。同时还提供了一个timeout参数, 如果没有任何时间到来, 那么旧最多等待timeout时间, 线程处于阻塞状态。一旦期间有任何事件到来, 就可以立即返回。时间过了之后还是没有任何事件到来,也会立即返回。拿到事件后, 线程就可以继续挨个处理相应的事件。处理完了继续过来轮询。于是线程就进入了一个死循环, 我们把这个死循环称为事件循环, 一个循环为一个周期。
每个客户端套接字socket都有对应的读写文件描述符。
read_events, write_events = select(read_fds, write_fds, timeout)
for event in read_events:
handle_read(event.fd)
for event in write_events:
handle_write(envent.fd)
# 处理其他事情,如定时任务
handle_others()
因为我们通过select系统调用同时处理多个通道描述符的读写事件, 因此我们将这类系统调用称为多路复用API。 现代操作系统的多路复用API已经不在使用select系统调用了, 而是改用epoll(linux)和kqueue(freebsd&macosc),因为select系统调用的吸能在描述符特别多时性能会非常差。他们使用起来可能在形式上略有差异,但是本质上都是差不多的,都可以用上述伪代码逻辑进行理解。
服务器套接字serversocket对象的读操作是指调用accpet接受客户端新连接。 何时有新连接到来,也是通过select系统调用的读事件来得到通知的。
事件轮询API就是Java语言里面的NIO技术。Java的NIO并不是Java特有的技术, 其他计算机语言都有这个技术, 只是换了一个词汇, 不叫NIO而已。
指令队列
Redis会将每隔客户端套接字都关联一个指令队列。客户端的指令通过队列来排队进行顺序处理,先到先服务。
响应队列
Redis同样会为每隔客户端套接字关联一个响应队列。Redis服务器通过响应队列来将指令的返回结果回复给客户端。 如果队列为空,那么意味着连接暂时处于空闲状态, 不要去获取读写事件, 也就是可以将当前的客户端描述符从write_fds里移出来。等到队列有数据了, 再将描述符放进去。避免select系统调用立即返回写事件,结果发现没什么数据可以写。出现这种情况的线程会飙高cpu。
定时任务
服务器处理要响应IO事件外,还要处理其它事情。比如定时任务就是非常重要的一件事。如果线程阻塞在select系统调用上,定时任务将无法得到准时调度。那么Redis是如何解决这个问题的呢?
Redis的定时任务会记录在一个称为最小堆的数据结构中。这个堆中, 最快要执行的任务排在堆的最上方。 在每隔循环周期,Redis都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是select系统调用的timeout参数。 因为Redis知道未来timeout时间内就, 没有其它定时任务需要处理,所以可以安心睡眠timeout时间。
Nginx和Node的时间处理原理和Redis也是类似的。