1. 理解单线程模型
Redis
基于Reactor
模式开发了自己的网络事件处理器,称之为文件事件处理器(File Event Hanlder
)。文件事件处理器由Socket
、IO
多路复用程序、文件事件分派器(dispather
),事件处理器(handler
)四部分组成。IO
多路复用程序会同时监听多个socket
,当被监听的socket
准备好执行accept
、read
、write
、close
等操作时,与这些操作相对应的文件事件就会产生。IO
多路复用程序会把所有产生事件的socket
压入一个队列中,然后有序地每次仅一个socket
的方式传送给文件事件分派器,文件事件分派器接收到socket
之后会根据socket
产生的事件类型调用对应的事件处理器进行处理。
文件事件处理器分为几种:
- 连接应答处理器:用于处理客户端的连接请求;
- 命令请求处理器:用于执行客户端传递过来的命令,比如常见的
set
、lpush
等; - 命令回复处理器:用于返回客户端命令的执行结果,比如
set
、get
等命令的结果;
事件种类:
AE_READABLE
:与两个事件处理器结合使用。- 当客户端连接服务器端时,服务器端会将连接应答处理器与
socket
的AE_READABLE
事件关联起来; - 当客户端向服务端发送命令的时候,服务器端将命令请求处理器与
AE_READABLE
事件关联起来;
- 当客户端连接服务器端时,服务器端会将连接应答处理器与
AE_WRITABLE
:当服务端有数据需要回传给客户端时,服务端将命令回复处理器与socket
的AE_WRITABLE
事件关联起来。
Redis
的客户端与服务端的交互过程:2. 为什么redis使用单线程模型还能保证高性能?
(1) 纯内存访问
redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,这是 redis 的 QPS 过万的重要基础。
(2) 非阻塞式IO
- 什么是阻塞式 IO
当我们调用 Scoket 的读写方法,默认它们是阻塞的。
read() 方法要传递进去一个参数 n,表示读取这么多字节后再返回,如果没有读够 n 字节线程就会阻塞,直到新的数据到来或者连接关闭了, read 方法才可以返回,线程才能继续处理。
write() 方法会首先把数据写到系统内核为 Scoket 分配的写缓冲区中,当写缓存区满溢,即写缓存区中的数据还没有写入到磁盘,就有新的数据要写道写缓存区时,write() 方法就会阻塞,直到写缓存区中有空闲空间。
- 什么是非阻塞式 IO
非阻塞 IO 在 Scoket 对象上提供了一个选项Non_Blocking
,当这个选项打开时,读写方法不会阻塞,而是能读多少读多少,能写多少写多少。
能读多少取决于内核为 Scoket 分配的读缓冲区的大小,能写多少取决于内核为 Scoket 分配的写缓冲区的剩余空间大小。读方法和写方法都会通过返回值来告知程序实际读写了多少字节数据。
有了非阻塞 IO 意味着线程在读写 IO 时可以不必再阻塞了,读写可以瞬间完成然后线程可以继续干别的事了。
(3) IO多路复用
文件描述符:内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。
多路 I/O 复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。
IO
复用只需要阻塞在select
,poll
或者epoll
,可以同时处理和管理多个连接。缺点是当select
、poll
或者epoll
管理的连接数过少时,这种模型将退化成阻塞IO
模型。并且还多了一次系统调用:一次select
、poll
或者epoll
一次recvfrom
。
最大连接数 | FD剧增后带来的IO效率问题 | 消息传递方式 | |
select | 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上FD_SETSIZE为32*64 | 因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。 | 内核需要将消息传递到用户空间,都需要内核拷贝动作 |
poll | 基于链表来存储的,没有最大连接数的限制 | 同上 | 内核需要将消息传递到用户空间,都需要内核拷贝动作 |
epoll | 接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接 | 因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。 | 利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销 |
(4) 单线程避免了线程切换和竞态产生的消耗。
单线程能带来几个好处:
- 第一,单线程可以简化数据结构和算法的实现。并发数据结构实现不但困难而且开发测试比较麻
- 第二,单线程避免了线程切换和竞态产生的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。
- 单线程的问题:对于每个命令的执行时间是有要求的。如果 某个命令执行过长,会造成其他命令的阻塞,所以 redis 适用于那些需要快速执行的场景。
参考:https://www.cnblogs.com/reecelin/p/13538382.html
https://blog.csdn.net/klarclm/article/details/8828486