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