Redis原理一:线程IO模型

Redis是个单线程程序!
也许你会怀疑可以支持海量数据、支持高并发的redis怎么可能是单线程。但是,事实上它就是,不要认为单线程就处理不了高并发。像Nginx的worker,它也是单线程。它们都是服务器高性能的典范。

单线程的redis为什么能这么快?

因为它所有的数据都在内存中,所以运算快。
因为它的IO是异步非阻塞IO
因为不是多线程,反而避免了多线程的频繁上下文切换问题

非阻塞IO

当我们使用套接字的读写方法,默认它们是阻塞的,比如我们使用sread和write。
read:数据在不超过指定的长度的时候有多少读多少,没有数据则会线程一直等待,直到新的数据到来或者连接关闭了,read方法才可以返回,线程才能继续处理。
而write一般来讲不会阻塞,除非内核为套接字分配的写缓冲区满了,write方法就会阻塞。

《Redis原理一:线程IO模型》

非阻塞 IO 在套接字对象上提供了一个选项Non_Blocking,当这个选项打开时,读写方法不会阻塞,而是能读多少读多少,能写多少写多少。能读多少取决于内核为套接字分配的读缓冲区内部的数据字节数,能写多少取决于内核为套接字分配的写缓冲区的空闲空间字节数。读方法和写方法都会通过返回值来告知程序实际读写了多少字节。

有了非阻塞 IO 意味着线程在读写IO时可以不必再阻塞了,读写可以瞬间完成然后线程可以继续干别的事了。

事件轮询 (多路复用)

非阻塞 IO 有个问题,那就是线程要读数据,结果读了一部分就返回了,线程如何知道何时才应该继续读。也就是当数据到来时,线程如何得到通知。写也是一样,如果缓冲区满了,写不完,剩下的数据何时才应该继续写,线程也应该得到通知。

《Redis原理一:线程IO模型》

事件轮询 API 就是用来解决这个问题的,最简单的事件轮询 API 是select函数,它是操作系统提供给用户程序的API。输入是读写描述符列表read_fds&write_fds,输出是与之对应的可读可写事件。同时还提供了一个timeout参数,如果没有任何事件到来,那么就最多等待timeout时间,线程处于阻塞状态。一旦期间有任何事件到来,就可以立即处理事件。时间过了之后还是没有任何事件到来,就会立即返回。拿到事件后,线程就可以继续挨个处理相应的事件。处理完了继续过来轮询。于是线程就进入了一个死循环,我们把这个死循环称为事件循环,一个循环为一个周期。

因为我们通过select系统调用同时处理多个通道描述符的读写事件,因此我们将这类系统调用称为多路复用 API。

现代操作系统的多路复用 API已经不再使用select系统调用,而改用epoll(linux)

事件轮询 API 就是 Java 语言里面的 NIO 技术

响应队列

Redis 同样也会为每个客户端套接字关联一个响应队列。Redis 服务器通过响应队列来将指令的返回结果回复给客户端。 如果队列为空,那么意味着连接暂时处于空闲状态,不需要去获取写事件,也就是可以将当前的客户端描述符从write_fds里面移出来。等到队列有数据了,再将描述符放进去。避免select系统调用立即返回写事件,结果发现没什么数据可以写。出这种情况的线程会飙高 CPU。

定时任务

服务器处理要响应 IO 事件外,还要处理其它事情。比如定时任务就是非常重要的一件事。如果线程阻塞在 select 系统调用上,定时任务将无法得到准时调度。那 Redis 是如何解决这个问题的呢?
Redis 的定时任务会记录在一个称为最小堆的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期,Redis 都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是select系统调用的timeout参数。因为 Redis 知道未来timeout时间内,没有其它定时任务需要处理,所以可以安心睡眠timeout的时间。

redis的线程模型

上面所描述的是相对简单的,我们细致的看一下redis的线程模型。

  1. redis基于reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器,,file event handler。这个文件事件处理器,是单线程的,redis才叫做单线程的模型,采用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器来处理这个事件。

  2. 文件事件处理器的结构包含4个部分:多个socket,IO多路复用程序,文件事件分派器,事件处理器(命令请求处理器、命令回复处理器、连接应答处理器,等等)

  3. 如果被监听的socket准备好执行accept、read、write、close等操作的时候,跟操作对应的文件事件就会产生,这个时候文件事件处理器就会调用之前关联好的事件处理器来处理这个事件。

  4. 多个socket可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个socket,但是会将socket放入一个队列中排队,每次从队列中取出一个socket给事件分派器,事件分派器把socket给对应的事件处理器。

  5. 然后一个socket的事件处理完之后,IO多路复用程序才会将队列中的下一个socket给事件分派器。文件事件分派器会根据每个socket当前产生的事件,来选择对应的事件处理器来处理。

  6. 当socket变得可读时(比如客户端对redis执行write操作,或者close操作),或者有新的可以应答的sccket出现时(客户端对redis执行connect操作),socket就会产生一个AE_READABLE事件。

  7. 当socket变得可写的时候(客户端对redis执行read操作),socket会产生一个AE_WRITABLE事件。

  8. IO多路复用程序可以同时监听AE_REABLE和AE_WRITABLE两种事件,要是一个socket同时产生了AE_READABLEAE_WRITABLE两种事件,那么文件事件分派器优先处理AE_REABLE事件,然后才是AE_WRITABLE事件。

《Redis原理一:线程IO模型》

点赞