目录

一、五种 I/O 模型

1.阻塞IO(Blocking IO)

2.非阻塞IO(Nonblocking IO)

3.IO多路复用(IO Multiplexing)

通知的方式

select模式

poll模式

epoll模式

4.信号驱动IO(Signal Driven IO)

5.异步IO(Asynchronous IO)

总结

二、Redis 

Redis为什么快?

Redis 线程模型

1.Redis6.0 之前为什么不使用多线程?

2.Redis6.0 之后为何引入了多线程?

3.Redis 后台线程了解吗?

为何Redis要使用I/O多路复用技术?

 IO多路复用原理

IO 多路复用的工作原理

文件事件处理器结构

Reactor设计模式

优点


Redis 是一个单线程却性能非常好的内存数据库, 主要用来作为缓存系统。 Redis 采用网络 I/O 多路复用技术来保证在多个连接时,系统的高吞吐量(TPS)。

系统吞吐量(TPS)指的是系统在单位时间内可处理的事务的数量,是用于衡量系统性能的重要指标。影响系统吞吐量的因素很多,包括并发数和系统资源(CPU、内存、系统I/O操作、外部接口)等,系统资源等这些因素可以用平均响应时间指标来衡量。

Socket套接字:对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。
例子:客户端在将数据通过网线发送到服务端的时候,客户端发送数据需要一个出口,
服务端接收数据需要一个入口,这两个“口子”就是Socket。

一、五种 I/O 模型

要理解 Redis 采用网络 I/O 多路复用技术,就得先了解五种 I/O 模型。

一个小例子:你是一个老师,让学生做作业,学生做完作业后收作业。
1)同步阻塞:逐个收作业,先收A,再收B,接着是C、D,如果有一个学生还未做完,
则你会等到他写完
,然后才继续收下一个。
2)同步非阻塞:逐个收作业,先收A,再收B,接着是C、D,如果有一个学生还未做完,
则你会跳过该学生,继续去收下一个
3)

selectpoll:学生写完了作业会举手,但是你不知道是谁举手,需要一个个的去询问
epoll:学生写完了作业会举手,你知道是谁举手,你直接去收作业

1.阻塞IO(Blocking IO)

最传统的一种 I/O 模型,即在读写数据过程中会发生阻塞现象。当使用 read 或者 write 对某一个文件描述符(File Descriptor 以下简称 FD)进行读写时,如果当前 FD 不可读或不可写,整个 Redis 服务就不会对其它的操作作出响应,导致整个服务不可用。

这也就是传统意义上的,也就是我们在编程中使用最多的阻塞模型。

具体流程:用户去读取数据时,会去先发起recvform一个命令,去尝试从内核上加载数据,如果内核没有数据,那么用户就会等待,此时内核会去从硬件上读取数据,内核读取数据之后,会把数据拷贝到用户态,并且返回ok,整个过程,都是阻塞等待的。

通俗来说,当用户线程发出 I/O 请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态(block),用户线程交出 CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除阻塞状态。data = socket.read();。如果数据没有就绪,用户线程就会一直阻塞在 read 方法。

阻塞模型虽然开发中非常常见也非常易于理解,但是由于它会影响其他 FD 对应的服务,所以在需要处理多个客户端任务的时候,往往都不会使用阻塞模型。 

2.非阻塞IO(Nonblocking IO)

具体流程:当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。

所以事实上,在非阻塞 I/O 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞 I/O 不会交出 CPU,而会一直占用 CPU,从而导致 CPU 占用率非常高。

3.IO多路复用(IO Multiplexing)

阻塞式的 I/O 模型并不能满足这里的需求,我们需要一种效率更高的 I/O 模型来支撑 Redis 的多个客户(redis-cli),这里涉及的就是 I/O 多路复用模型了。多路复用 I/O 模型是目前使用得比较多的 I/O 模型。

IO多路复用是利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。

文件描述符(File Descriptor):简称FD,是一个从0 开始的无符号整数,用来关联Linux中的一个文件。在Linux中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket)。

IO多路复用的解释,即使用一个或一组线程处理多个tcp连接。

  • IO指代网络IO,需要进行模态转换的读写操作
  • 多路,多个客户端连接(socket)
  • 复用,复用一个或多个线程 

在多路复用 I/O 模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 I/O 读写操作。因为在多路复用 I/O 模型中,只需要使用一个线程就可以管理多个 socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程。并且只有在真正有 socket 读写事件进行时,才会使用 I/O 资源,所以它大大减少了资源占用(如 CPU)。 

当如下任一情况发生时,会产生套接字的可读事件:

  • 该套接字的接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的大小;
  • 该套接字的读半部关闭(也就是收到了FIN),对这样的套接字的读操作将返回0(也就是返回EOF);
  • 该套接字是一个监听套接字且已完成的连接数不为0;
  • 该套接字有错误待处理,对这样的套接字的读操作将返回-1。

当如下任一情况发生时,会产生套接字的可写事件:

  • 该套接字的发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的大小;
  • 该套接字的写半部关闭,继续写会产生SIGPIPE信号;
  • 非阻塞模式下,connect返回之后,该套接字连接成功或失败;
  • 该套接字有错误待处理,对这样的套接字的写操作将返回-1。

会思考的你也许会想到,可以采用多线程+ 阻塞 I/O 达到类似的效果,但是由于在多线程 + 阻塞 I/O 中,每个 socket对应一个线程,这样会造成很大的资源占用,并且尤其是对于长连接来说,线程的资源一直不会释放,如果后面陆续有很多连接的话,就会造成性能上的瓶颈。

而多路复用 I/O 模式,通过一个线程就可以管理多个 socket,只有当 socket 真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用IO比较适合连接数比较多的情况。

具体流程:通过FD,我们的网络模型可以利用一个线程监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。当用户去读取数据的时候,不再去直接调用recvfrom了,而是调用select的函数,select函数会将需要监听的数据交给内核,由内核去检查这些数据是否就绪了。如果说这个数据就绪了,就会通知应用程序数据就绪,然后来读取数据,再从内核中把数据拷贝给用户态,完成数据处理,如果N多个FD一个都没处理完,此时就进行等待。

使用IO复用模式,可以确保去读数据的时候,数据是一定存在的,它的效率比原来的阻塞IO和非阻塞IO性能都要高。

另外,多路复用 I/O 为何比非阻塞 I/O 模型的效率高?是因为在非阻塞 I/O 中,不断地询问 socket 状态时通过用户线程去进行的。而在多路复用 I/O 中,轮询每个 socket 状态是内核在进行的,这个效率要比用户线程要高的多。

值得注意的是,多路复用 I/O 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用 I/O 模型来说,一旦事件响应太慢,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。

这就是说,如果 Redis 每条命令执行如果占用大量时间,就会造成其他线程阻塞,对于 Redis 这种高性能服务是致命的,所以 Redis 是面向高速执行的数据库。

通知的方式

一个服务端进程可以同时处理多个socket,IO多路复用模型包括select,poll,epoll。

select模式

Select模型:使用select函数来监听多个文件描述符(包括网络连接),一旦有一个或多个文件描述符就绪,就会通知Redis进行相应的IO操作。

简单说,就是我们把需要处理的数据封装成FD,然后在用户态时创建一个FD的集合(这个集合的大小是要监听的那个FD的最大值+1,但是大小整体是有限制的 ),这个集合的长度大小是有限制的。同时在这个集合中,标明出来我们要控制哪些数据。

比如,要监听的数据,是1,2,5三个数据,此时会执行select函数,然后将整个fd发给内核态。内核态会去遍历用户态传递过来的数据,如果发现这里边都数据都没有就绪,就休眠,直到有数据准备好时,就会被唤醒。唤醒之后,再次遍历一遍,看看谁准备好了,然后再将处理掉没有准备好的数据。最后再将这个FD集合写回到用户态中去,此时用户态就知道此时有人准备好了。但是对于用户态而言,并不知道谁处理好了,所以用户态也需要去进行遍历,然后找到对应准备好数据的节点,再去发起读请求。

优点是:不需要每个FD都进行一次系统调用,解决了频繁的用户态内核态切换的问题。

我们会发现,这种模式虽然比阻塞IO和非阻塞IO好,但是依然有些麻烦的事情, 比如说频繁的传递fd集合,频繁的去遍历FD等问题。因此,存在的三个问题:

  • 单进程监听的FD存在限制,最大不超过1024。
  • 每次select都需要把所有要监听的FD都拷贝到内核空间。
  • 不知道具体是哪个文件描述符就绪,每次都要遍历所有FD来判断就绪状态。
poll模式

Poll模型与Select类似,但性能更优。它使用poll函数来监听并处理多个文件描述符。

poll模式对select模式做了简单改进,

  • select模式中的fd_set大小固定为1024,而pollfd在内核中采用链表,理论上无上限。
  • 监听FD越多,每次遍历消耗时间也越久,性能反而会下降。

具体的IO流程:

  1. 创建pollfd数组,向其中添加关注的fd信息,数组大小自定义。
  2. 调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限。
  3. 内核遍历fd,判断是否就绪。
  4. 数据就绪或超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n。
  5. 用户进程判断n是否大于0,大于0则遍历pollfd数组,找到就绪的fd。

存在的问题:poll利用链表解决了select中监听FD上限的问题,但依然要遍历所有FD,如果监听较多,性能会下降。

epoll模式

Epoll模型只适用于Linux系统,在高并发场景下性能最好。它使用epoll函数来监听多个文件描述符,并使用事件驱动的方式处理IO操作。

epoll模式提供了三个函数:

1)eventpoll(),包括红黑树和一个链表。红黑树主要记录要监听的FD,链表主要记录就绪的FD。

2)epoll_ctl的作用是当你对一个新的fd的读/写事件感兴趣时,通过该调用将fd与相应的感兴趣事件更新到context中。

调用epoll_ctl()操作,可以将要监听的数据添加到红黑树上去,并且给每个fd设置一个监听函数。这个函数会在fd数据就绪时触发,也就是准备好了,就会把fd把数据添加到list_head中去。

3)epoll_wait的作用是等待context中fd的事件发生。

调用epoll_wait(),就去等待,在用户态创建一个空的events数组,当就绪之后,我们的回调函数会把数据添加到list_head中去,当调用这个函数的时候,会去检查list_head,当然这个过程需要参考配置的等待时间,可以等一定时间,也可以一直等, 如果在此过程中,检查到了list_head中有数据会将数据添加到链表中,此时将数据放入到events数组中,并且返回对应的操作的数量,用户态的此时收到响应后,从events中拿到对应准备好的数据的节点,再去调用方法去拿数据。

epoll 其实只是众多实现 I/O多路复用模型的技术当中的一种而已,但是相比其他 I/O 多路复用技术(select, poll等),epoll有诸多优点(Redis 也支持 select 和 poll,默认使用 epoll):

  1. epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048。
  2. 效率提升, epoll 最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中, epoll 的效率就会远远高于 select 和 poll。
  3. 内存拷贝, epoll 直接使用的 "共享内存",可以跳过传统的内存拷贝操作,效率更高。

如何解决select和poll存在的这些问题的?

epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd。

  • 基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高。
  • 每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间。
  • 利用ep_poll_callback机制来监听FD状态,无需遍历所有FD,因此性能不会随监听的FD数量增多而下降。

4.信号驱动IO(Signal Driven IO)

在信号驱动 I/O 模型中,当用户线程发起一个 I/O 请求操作,会给对应的 socket 注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用 I/O 读写操作来进行实际的 I/O 请求操作。

这个一般用于 UDP 中,对 TCP 套接口几乎是没用的,原因是该信号产生得过于频繁,并且该信号的出现并没有告诉我们发生了什么事情。

5.异步IO(Asynchronous IO)

异步 I/O 模型才是最理想的 I/O 模型。

在异步 I/O 模型中,当用户线程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它收到一个 asynchronous read 之后,它会立刻返回,说明 read 请求已经成功发起了,因此不会对用户线程产生任何阻塞(block)。

然后,内核会等待数据准备完成,然后将数据拷贝到用户线程。当这一切都完成之后,内核会给用户线程发送一个信号,告诉它 read 操作完成了。

也就是说,用户线程完全不需要关心实际的整个 I/O 操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示 I/O 操作已经完成,可以直接去使用数据了。

这种方式,不仅仅是用户态在试图读取数据后不阻塞,而且当内核的数据准备完成后,也不会阻塞。

具体流程:它会由内核将所有数据处理完成后,由内核将数据写入到用户态中,然后才算完成,所以性能极高,不会有任何阻塞,全部都由内核完成,可以看到,异步IO模型中,用户进程在两个阶段都是非阻塞状态。

总结

这五种 I/O 模型中,前面四种 I/O 模型实际上都属于同步 I/O,只有最后一种是真正的异步 I/O。

因为无论是多路复用 I/O模型还是信号驱动 I/O 模型,I/O 操作的第 2 个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。

二、Redis 

Redis为什么快?

Redis 之所以快速,主要有以下几个原因:

  1. 内存存储:Redis 将所有数据存储在内存中,内存的读写速度远快于磁盘,这使得 Redis 在处理请求时能够迅速响应。

  2. 单线程模型:Redis 使用单线程处理请求,避免了多线程中的上下文切换和锁竞争问题。虽然看似单线程可能导致性能瓶颈,但 Redis 的事件驱动模型和高效的 I/O 操作使得它在处理大量请求时仍然能保持高性能。

  3. 高效的数据结构:Redis 提供了多种高效的数据结构(如字符串、哈希、列表、集合、有序集合等),这些数据结构针对特定操作进行了优化,能快速处理不同类型的数据请求。

  4. 持久化机制:虽然 Redis 是内存数据库,但它提供了快照和 AOF(Append Only File)两种持久化机制,可以在确保数据持久化的同时,保持高性能。

  5. 客户端缓存:Redis 支持发布/订阅模式,能够有效减少重复请求和不必要的数据传输,进一步提高响应速度。

  6. 高效的网络协议:Redis 使用自定义的二进制协议,减少了网络传输时的数据包大小和解析时间,提高了网络 I/O 的效率。

  7. 丰富的命令和功能:Redis 提供了丰富的内置命令,针对常用操作进行了优化,减少了开发者在应用层的处理时间。

通过以上这些机制,Redis 能够在处理大规模数据时保持高效和快速的性能表现。

Redis 线程模型

Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型 (Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。

  • 文件事件处理器使用 I/O 多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

优点:虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。

组成:

(1) 多个 socket(客户端连接)

(2)IO 多路复用程序(支持多个客户端连接的关键)

既然是单线程,那怎么监听大量的客户端连接呢?Redis 通过 IO 多路复用程序监听来自客户端的多个套接字,并向文件事件派发器传递那些产生了事件的套接字。

尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生的套接字都放到同一个队列(aeEventLoopfired就绪事件表)里,然后文件事件处理器会以有序、同步、单个套接字的方式处理该队列中的套接字,也就是处理就绪的文件事件。

好处:I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector 组件很像)。

(3)文件事件分派器(将 socket 关联到相应的事件处理器)

(4) 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

所以,一次 Redis 客户端与服务器进行连接并且发送命令的过程:

  • 客户端向服务端发起建立 socket 连接的请求,那么监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行。处理器会对客户端的连接请求进行应答,然后创建客户端套接字,以及客户端状态,并将客户端套接字的 AE_READABLE 事件与命令请求处理器关联。
  • 客户端建立连接后,向服务器发送命令,那么客户端套接字将产生 AE_READABLE 事件,触发命令请求处理器执行,处理器读取客户端命令,然后传递给相关程序去执行。
  • 执行命令获得相应的命令回复,为了将命令回复传递给客户端,服务器将客户端套接字的 AE_WRITEABLE 事件与命令回复处理器关联。当客户端试图读取命令回复时,客户端套接字产生 AE_WRITEABLE 事件,触发命令回复处理器将命令回复全部写入到套接字中。

1.Redis6.0 之前为什么不使用多线程?

虽然说 Redis 是单线程模型,但实际上,Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。

不过,Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主线程之外的其他线程来“异步处理”,从而减少对主线程的影响。

为此,Redis 4.0 之后新增了几个异步命令:

  • UNLINK:可以看作是 DEL 命令的异步版本。
  • FLUSHALL ASYNC:用于清空所有数据库的所有键,不限于当前 SELECT 的数据库。
  • FLUSHDB ASYNC:用于清空当前 SELECT 数据库中的所有键。

总的来说,直到 Redis 6.0 之前,Redis 的主要操作仍然是单线程处理的。

那 Redis6.0 之前为什么不使用多线程? 我觉得主要原因有 3 点:

  • 单线程编程容易并且更容易维护;
  • Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
  • 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。

2.Redis6.0 之后为何引入了多线程?

Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。

我们可以在 Redis 在中使用 DEL 命令来删除一个键对应的值,如果待删除的键值对占用了较小的内存空间,那么哪怕是同步地删除这些键值对也不会消耗太多的时间。

但是对于 Redis 中的一些超大键值对,几十 MB 或者几百 MB 的数据并不能在几毫秒的时间内处理完,Redis 可能会需要在释放内存空间上消耗较多的时间,这些操作就会阻塞待处理的任务,影响 Redis 服务处理请求的 PCT99 和可用性。

虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。

Redis6.0 的多线程默认是禁用的,只使用主线程。

  • 如需开启需要设置 IO 线程数 > 1,需要修改 redis 配置文件 redis.conf
    • io-threads 的个数一旦设置,不能通过 config 动态设置。
    • 当设置 ssl 后,io-threads 将不工作。
  • 开启多线程后,默认只会使用多线程进行 IO 写入 writes,即发送数据给客户端。如果需要开启多线程 IO 读取 reads,同样需要修改 redis 配置文件 redis.conf
io-threads 4 #设置1的话只会开启主线程,官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程
io-threads-do-reads yes

3.Redis 后台线程了解吗?

虽然经常说 Redis 是单线程模型(主要逻辑是单线程完成的),但实际还有一些后台线程用于执行一些比较耗时的操作:

  • 通过 bio_close_file 后台线程来释放 AOF / RDB 等过程中产生的临时文件资源。
  • 通过 bio_aof_fsync 后台线程调用 fsync 函数将系统内核缓冲区还未同步到到磁盘的数据强制刷到磁盘( AOF 文件)。
  • 通过 bio_lazy_free后台线程释放大对象(已删除)占用的内存空间。

 

为何Redis要使用I/O多路复用技术?

Redis 是单线程架构,所有的命令操作都是先进入队列,然后一个一个按照顺序线性执行的。也就是说Rdis是跑在单线程中的,所有的操作都是按照顺序线性执行的。但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而采用 I/O 多路复用技术就是为了解决这个问题。

一句话总结就是,Redis 采用 I/O 多路复用技术(epoll)是因为 Redis 是单线程架构,是为了避免网络 I/O 读写操作阻塞整个进程。

那Redis 为何不采用异步 I/O 模型,这个不是效率更高吗?这玩意儿在多线程下才能发挥功效,而 Redis 是单线程架构。

 IO多路复用原理

所谓I/O多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要select、pol川、epol来配合。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。

Redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。

IO 多路复用的工作原理

IO 多路复用的工作原理如下:

  • 事件注册:当新的客户端连接建立时,Redis 会为这个连接创建一个套接字,并将其对应的文件描述符注册到 IO 多路复用程序中,通常使用的是 epoll(在 Linux 系统中)或其他平台的类似机制(如 kqueue、evport)。

  • 事件监听:IO 多路复用程序会持续监控所有注册的文件描述符,等待读、写、连接等事件的发生。这一过程是非阻塞的,即它不会因为某个描述符未就绪而阻塞整个监控过程。

  • 事件分发:一旦有事件准备就绪(如客户端发送了数据,可以读取;或者服务器准备好发送数据到客户端,可以写入),IO 多路复用程序会将该文件描述符标记为活跃状态,并通过事件队列告知文件事件分派器。

  • 事件处理:文件事件分派器根据事件类型和关联的事件处理器,分发控制权给对应的处理器执行实际操作,如读取数据、写入响应、处理新连接请求或关闭连接等。

下图就是基于多路复用的redis IO模型。图中的多个FD就是刚才所说的多个套接字。Redis网络框架调用epoll机制,让内核监听这些套接字。此时Redis线程不会阻塞在某一个特定的监听或者已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,redis可以同时和多个客户端连接并处理请求,从而提升并发性。

为了在请求到达时能够通知到redis线程,select/epoll提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。

那么,回调机制是怎么工作的?其实select/epoll一旦监听到FD上有请求到达时,就会触发相应的事件。这些事件会被放进一个事件队列,redis单线程对该事件队列不断进行处理。这样一来,redis无需一直轮询是否有请求实际发生,这就可以避免造成CPU资源浪费。同时redis在对事件队列中的事件进行处理时,会调用相应处理函数。这就实现了基于事件的回调。因为redis一直在对时间队列进行处理,所以能够及时响应客户端请求,提升redis的响应性能。

通俗的说,当我们的客户端想要去连接我们服务器,会去先到IO多路复用模型去进行排队,会有一个连接应答处理器去接受读请求,然后又把读请求注册到具体模型中去。此时这些建立起来的连接,如果是客户端请求处理器去进行执行命令时,它会去把数据读取出来,然后把数据放入到client中。clinet去解析当前的命令转化为redis认识的命令,接下来就开始处理这些命令,从redis中的command中找到这些命令,然后就真正的去操作对应的数据了。当数据操作完成后,会去找到命令回复处理器,再由它将数据写出。

文件事件处理器结构

Redis 的文件事件处理器主要包括以下几个组成部分:

  • Socket:用于客户端与服务端的网络通信。
  • IO 多路复用程序:核心组件,负责监控多个文件描述符(FD,包括套接字描述符)的读写事件。
  • 文件事件分派器:接收 IO 多路复用程序的通知,根据事件类型调度相应的事件处理器。
  • 事件处理器:执行具体的读、写、连接应答、关闭等操作。

Reactor设计模式

Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符)。

文件事件处理器使用 I/O 多路复用模块同时监听多个 FD,当 accept、read、write 和 close 文件事件产生时,文件事件处理器就会回调 FD 绑定的事件处理器。

虽然整个文件事件处理器是在单线程上运行的,但是通过 I/O 多路复用模块的引入,实现了同时对多个 FD 读写的监控,提高了网络通信模型的性能,同时也可以保证整个 Redis 服务实现的简单。

优点

高并发处理能力:通过单线程模型配合 IO 多路复用,Redis 能够高效地处理成千上万个并发连接,避免了线程上下文切换的开销。

资源效率:由于所有操作在一个线程内完成,减少了线程间的竞争和同步开销。

可扩展性和可移植性:Redis 实现了针对不同平台的 IO 多路复用库的抽象,使得其底层实现可以根据操作系统特性灵活选择,保证了代码的可移植性。

需要注意的是,Redis IO多路复用是一种异步的IO模型,适用于处理大量的小数据传输。在有大量的大数据传输的情况下,IO多路复用可能并不能得到很好的性能提升。此时可以考虑使用其他更适合的IO模型,如多线程或异步IO等。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部