Redis 线程模型

  Redis 是个单线程程序。除了redis,nginx、node.js 也是单线程程序,但是它们都是服务器高性能的典范。

  Redis 单线程为什么还能这么快?因为所有的数据都在内存中,所有的运算都是内存级别的运算。正因为Redis是单线程程序,所以要小心使用Redis 指令,对于那些时间复杂度为O(n) 级别的指令,一定要谨慎使用,否则会一步小心造成Redis 卡顿。

  Redis 既然是单线程,如何能处理那么多的并发客户端连接?因为"多路复用", 其原理是select 系列的事件轮询API以及非阻塞IO。

  Redis的单线程是说做核心处理的是单线程,还有一些其他线程在处理一些其他任务,比如删除过期的key、主从数据同步等。

1. 非阻塞IO

  socket 默认是阻塞的,比如read 方法要传递进去一个参数n标识最多读取n个字节后再返回,如果一个字节都没有就会卡在那里,直到新的数据到来或者链接关闭,read 方法才可以返回, 线程才能继续处理。write 方法一般来说不会阻塞,除非内核为套接字分配的写缓存区已经满了,write 方法就会阻塞, 直到缓存区有空间空闲出来。

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

查看linux 下面呢的socket 命令:

SOCKET(2)                                   Linux Programmer's Manual                                   SOCKET(2)

NAME
       socket - create an endpoint for communication

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int socket(int domain, int type, int protocol);

DESCRIPTION
       socket() creates an endpoint for communication and returns a descriptor.

       The  domain argument specifies a communication domain; this selects the protocol family which will be used
       for communication.  These families are  defined  in  <sys/socket.h>.   The  currently  understood  formats
       include:

       Name                Purpose                          Man page
       AF_UNIX, AF_LOCAL   Local communication              unix(7)
       AF_INET             IPv4 Internet protocols          ip(7)
       AF_INET6            IPv6 Internet protocols          ipv6(7)
       AF_IPX              IPX - Novell protocols
       AF_NETLINK          Kernel user interface device     netlink(7)
       AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
       AF_AX25             Amateur radio AX.25 protocol
       AF_ATMPVC           Access to raw ATM PVCs
       AF_APPLETALK        Appletalk                        ddp(7)
       AF_PACKET           Low level packet interface       packet(7)

       The  socket  has the indicated type, which specifies the communication semantics.  Currently defined types
       are:

       SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.  An out-of-band data
                       transmission mechanism may be supported.
       SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages of a fixed maximum length).

       SOCK_SEQPACKET  Provides  a sequenced, reliable, two-way connection-based data transmission path for data‐
                       grams of fixed maximum length; a consumer is required to read an entire packet  with  each
                       input system call.

       SOCK_RAW        Provides raw network protocol access.

       SOCK_RDM        Provides a reliable datagram layer that does not guarantee ordering.

       SOCK_PACKET     Obsolete and should not be used in new programs; see packet(7).

       Some  socket  types  may  not  be implemented by all protocol families; for example, SOCK_SEQPACKET is not
       implemented for AF_INET.

       Since Linux 2.6.27, the type argument serves a second purpose: in addition to specifying a socket type, it
       may include the bitwise OR of any of the following values, to modify the behavior of socket():

       SOCK_NONBLOCK   Set  the  O_NONBLOCK  file  status flag on the new open file description.  Using this flag
                       saves extra calls to fcntl(2) to achieve the same result.

       SOCK_CLOEXEC    Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor.  See  the  description
                       of the O_CLOEXEC flag in open(2) for reasons why this may be useful.

       The  protocol specifies a particular protocol to be used with the socket.  Normally only a single protocol
       exists to support a particular socket type within a given protocol family, in which case protocol  can  be
       specified  as 0.  However, it is possible that many protocols may exist, in which case a particular proto‐
       col must be specified in this manner.  The protocol number  to  use  is  specific  to  the  “communication
       domain”  in which communication is to take place; see protocols(5).  See getprotoent(3) on how to map pro‐
       tocol name strings to protocol numbers.

。。。

RETURN VALUE
       On  success, a file descriptor for the new socket is returned.  On error, -1 is returned, and errno is set
       appropriately.

2. 事件轮询 - 多路复用

  非阻塞IO有个问题,那就是线程要读数据,结果读了一部分就返回,那么线程如何才应该继续读---也就是当数据到来时,线程如何得到通知,写也是一样。

  事件轮询API就是用来解决这个问题的。最简单的事件轮询API是select 函数,也就是多路复用器。关于多路复用器,后来内核继续发展提供了poll、epoll(linux)、kqueue(FreeBSD 和 macosx)。

  事件轮询API就是Java 预言里面的NIO技术。 Java 的NIO并不是Java 特有的技术,其他计算机语言都有这个技术,只不过换了一个词汇,不叫NIO而已。

  epoll 包含三个子函数(epoll_create 创建一个epoll 实例,epoll 实例包含维护事件描述符的数据结构; epoll_ctl 添加事件; epoll_wait 获取就绪的事件)。

3. 指令队列

  Redis 会为每个客户端套接字都关联一个指令队列。客户端的指令通过队列来排队进行顺序处理,先到先服务。

4. 响应队列

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

5. 定时任务

  服务器除了响应IO事件外,还要处理其他事情。比如定时任务就是非常重要的一件事。如果线程阻塞在系统调用上,定时任务将无法得到准时调度。

  Redis 的定时任务会记录在一个被称为"最小堆"的数据结构中。在这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期里。Redis 都会对最小堆里面已经到时间点的任务进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是select 系统调用的timeout 参数。因为Redis 知道未来timeout 的值的时间内,没有其他定时任务需要处理,所以可以安心睡眠timeout的值的时间。

  Nginx和Node的事件处理原理和Redis 也是类似的。

  关于epoll 多路复用器以及redis 单线程的验证参考:https://www.cnblogs.com/qlqwjy/p/15023277.html

【当你用心写完每一篇博客之后,你会发现它比你用代码实现功能更有成就感!】
原文地址:https://www.cnblogs.com/qlqwjy/p/14947843.html