详细介绍了单线程Reactor模式的概念,以及Redis的线程模型—文件事件处理器的实现。
Redis的线程模型是基于非常经典的单线程Reactor模式(netty架构也是基于Reactor模式)开发出的高效事件驱动模型,也称为异步阻塞IO或者IO多路复用。关于Reactor模型我们在此前就讲过了: Java NIO Reactor网络编程模型的深度理解。
Redis对于单线程Reactor模式的具体的实现就是Redis中的文件事件处理器(file event handler,FEH)。
1 Reactor模式
关于Reactor模型我们在此前就讲过了:Java NIO Reactor网络编程模型的深度理解。
在传统阻塞 IO 模型中,每个连接都需要独立线程处理,当并发数大时,创建线程数多,占用资源;采用阻塞 IO 模型,连接建立后,若当前线程没有数据可读,线程会阻塞在读操作上,造成资源浪费
针对传统阻塞 IO 模型的两个问题,Reactor 模型基于池化思想,避免为每个连接创建线程,连接完成后将业务处理交给线程池处理(多线程Reactor的思路);基于 IO 复用模型,多个连接共用同一个阻塞对象Reactor。Reactor遍历到有新数据可以处理时,操作系统会通知程序,线程跳出阻塞状态,进行业务逻辑处理。
IO多路复用:多路指的是多个socket连接,复用指的是复用一个线程,采用多路I/O复用技术可以让单个线程高效的处理多个连接请求(减少线程资源的消耗以及等待网络IO时间的消耗)。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。
下面是单线程Reactor模式:
三个角色:
- Reactor:专门用于监听和响应各种IO事件,比如连接建立就绪(ACCEPT)、读就绪(READ)、写就绪(WRITE)等,当检测到一个新的事件到来时,将其发送给相应的Handler去处理。
- Handler:专门用于处理特定的事件,比如读取数据,业务逻辑执行,写出响应等。
- Acceptor:专门用于处理建立连接事件,可以看做是一个特殊的Handler。
总体流程为:
- 服务端的Reactor 线程对象通过循环 select调用(IO 多路复用)监听各种IO事件,还会注册一个accepter事件处理器到Reactor中,accepter专用于处理建立连接事件。
- 客户端首先发起一个建立连接的请求,Reactor监听到ACCEPT事件的到来后将该ACCEPT事件分派给accepter组件,accepter通过accept()方法与客户端建立对应的连接(SocketChannel),然后将该连接所关注的READ事件以及对应的READ事件处理器注册到Reactor中,这样Reactor就会监听该连接的READ事件。
- 当Reactor监听到该连接有读或者写事件发生时,将相关的事件派发给对应的处理器进行处理。比如,读处理器会通过SocketChannel的read()方法直接读取到数据,随后可进行各种业务处理,之后需要向客户端发送数据时,也可以注册该连接的WRITE事件和其对应的处理器,当channel可写时,通过SocketChannel的wtite()方法写数据。
- 每当处理完所有就绪的IO事件后,Reactor线程会再次执行select()操作阻塞等待新的事件就绪并将其分派给对应处理器进行处理。
单线程Reactor模型中,所有的操作都在一个线程中处理,如果某个 handler 阻塞时,会导致其他所有的 client 的 handler 都得不到执行,更严重的是会导致整个服务不能接收新的 client 请求(因为 acceptor 也被阻塞了),因为有这么多的缺陷,因此单线程Reactor 模型用的比较少,但是却很适合Redis的网络模型,因为Redis要求保证每个操作的原子性。
2 文件事件处理器
Redis对于Reactor模型的实现为文件事件处理器,并且采用单线程Reactor模型。
2.1 基本概念
文件事件处理器大概由四个部分组成:多个Socket(连接)、IO多路复用程序(类似于Java NIO中的Selector)、文件事件分派器、事件处理器。
Socket可以被看作一个客户端和Redis服务器的连接通道,客户端和Redis服务器通过Socket互相传递数据。一个服务器通常会连接多个socket(客户端),多个socket可能并发产生不同操作,每个操作对应不同文件事件。
Redis的I/O 多路复用程序使用IO多路复用技术(类比于Java NIO 中的 Selector),它是是通过包装select、epoll、evport和kqueue这些I/O多路复用函数库来实现的。只需要一个线程就可以监听多个客户端Socket(将socket的fd注册到epoll函数即可实现监听),相比于阻塞IO的一个Socket对应一个线程,降低了资源消耗。
文件事件就是对socket操作的抽象,当被监听的Socket准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生。由于会监听多个socket,因此尽管文件事件可能并发出现,但 I/O 多路复用程序会将所有产生事件的socket放入同一个队列,通过该队列以有序、同步且每次一个socket的方式向文件事件分派器传送socket。虽然多个客户端发送的命令的执行顺序是不确定的,但一定不会有两条命令被同时执行,不会产生并行问题。
I/O 多路复用程序可以监听socket的AE_READABLE事件和AE_WRITABLE事件:
- 当Socket变得可读时(客户端对Socket执行write操作,或者执行close操作),或者有新的可应答(acceptable)Socket出现时(客户端对服务器的监听Socket执行connect连接操作),Socket产生AE_READABLE 事件。
- 当Socket变得可写时(客户端对Socket执行read操作),Socket产生AE_WRITABLE事件。
I/O多路复用程序允许服务器同时监听Socket的AE_READABLE事件和AE_WRITABLE事件,如果一个Socket同时产生了这两种事件,那么文件事件分派器会优先处理AE_READABLE事件,等到AE_READABLE事件处理完之后,才处理AE_WRITABLE 事件。这也就是说,如果一个Socket又可读又可写的话,那么服务器将先读Socket,后写Socket。
文件事件分派器从队列中接收 I/O 多路复用程序传来的socket,并根据socket产生的事件类型,调用相应的事件处理器。当上一个socket产生的事件被对应事件处理器执行完后,文件事件分派器才会向队列拉取下一个要处理的socket,保证socket操作一定不会并发的执行,保证线程安全。
服务器会为执行不同任务的Socket关联不同的事件处理器,这些处理器实际上就是一个个函数,它们定义了某个事件发生时,服务器应该执行的某些动作,常见处理器:
- 客户端请求与Redis服务器建立连接时,为了对连接服务器的各个客户端进行应答,需要将socket映射到连接应答处理器。
- 客户端向Redis服务器发送命令时,为了接收客户端传来的命令请求,就需要将socket映射到命令请求处理器。
- 客户端从Redis服务器读取结果数据时,为了向客户端返回命令的执行结果,就需要将socket映射到命令回复处理器。
- 当主服务器和从服务器进行复制操作时, 主从服务器都需要映射到特别为复制功能编写的复制处理器。
2.2 通信流程
下面是客户端和redis通过文件事件处理器进行通信的大概流程:
下面是一次完整的客户端与服务器连接和一次通信流程示例:
- 当Redis服务器进行启动初始化的时候,程序会将连接应答处理器和服务器连接监听Socket(Server Socket)的AE_READABLE事件关联起来。
- 当有客户端连接服务器监听Socket的时候,Socket就会产生AE_READABLE 事件,引发连接应答处理器执行, 并执行相应的Socket应答操作:创建客户端对应的Socket,同时将这个客户端Socket的AE_READABLE事件和命令请求处理器关联,使得客户端可以向主服务器发送命令请求,到此客户端到服务器的Socket连接建立完毕。
- 客户端向Redis服务器发送一个命令请求(无论是读命令还是写命令),那么客户端Socket将产生 AE_READABLE事件,引发命令请求处理器执行,处理器读取客户端的命令内容,然后对命令进行执行。
- 命令执行将会产生命令回复(结果),为了将这些命令回复传送回客户端,当Redis服务器准备好给客户端的响应数据后,服务器会将客户端Socket的AE_WRITABLE事件与命令回复处理器进行关联:当客户端尝试读取命令回复的时候,客户端Socket将产生AE_WRITABLE事件, 触发命令回复处理器执行,将命令回复(结果)数据写入Socket,这样一来客户端就可以读取结果。
- 当命令回复处理器将命令回复全部写入到Socket之后,服务器就会解除客户端Socket的AE_WRITABLE事件与命令回复处理器之间的关联。
文件事件处理器的整个处理过程都是单线程进行的,这个过程也就是Redis处理网络请求、执行客户端命令的过程,我们一般说的Redis是单线程的,就是由于文件事件处理器(file event handler)是单线程方式运行的,我们把该线程称为主线程,实际上,Redis内部还有许多其他辅助线程,但是不会用于处理客户端请求。
从上面的流程可以看到,虽然文件事件处理器是单线程运行的,但通过使用 I/O 多路复用程序,使用一个线程就可以同时监听多个Socket(连接),既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。
另外,可以看到一条命令的的读取和返回都是异步的,一个命令的读取和返回之间还能够执行其他的命令,这样的拆分可以防止某一个客户端的IO准备过程过慢而阻塞其他客户端的操作。
Redis中还有一种时间事件,主要用于定期更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等;清理数据库中的过期键值对;对不合理的数据库进行大小调整;关闭和清理连接失效的客户端;尝试进行 AOF 或 RDB 持久化操作;如果服务器是主节点的话,对附属节点进行定期同步;如果处于集群模式的话,对集群进行定期同步和连接测试。
相关文章:
- https://redis.io/topics/data-types
- https://redis.io/topics/data-types-intro
如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!