Redis知识点总结(二)——Redis高性能IO模型及其事件驱动框架剖析
- IO多路复用
- 传统的阻塞式IO
- 同步非阻塞IO
- IO多路复用机制
- Redis的IO模型
- Redis的事件驱动框架
IO多路复用
Redis的高性能的秘密,在于它底层使用了IO多路复用这种高性能的网络IO,IO多路复用是一种高性能的IO机制。
传统的阻塞式IO
传统的同步阻塞式IO,一个IO连接对应一个线程,也就是说一个线程只能监听一个IO连接,并且在没有数据到达前,这个线程会阻塞等待数据的到达,在数据到达后,才把数据读取到应用程序的内存区域。
比如我们熟悉的socket编程,就是典型的同步阻塞式网络IO,ServerSocket的accept()方法监听指定端口接收客户端建立连接,此时当前线程会阻塞,等待客户端发起连接。建立连接后返回一个Socket表示与客户端建立了一个连接,然后我们创建一个线程Thread,在线程中调用Socket的read()方法,等待客户端发送数据,如果客户端迟迟不发送数据,那么该线程将会一直被阻塞。
这是一种性能比较低的IO机制,原因在于一个线程只能监听一个连接,并且在数据没有到达之前,需要阻塞等待。
同步非阻塞IO
而同步非阻塞式IO则不一样,它可以尝试性的读取看看是否有数据,如果没有数据可以立即返回,不会阻塞,当前线程可以去干点别的事情,然后再次回来尝试读取,如果发现有数据到达,才会阻塞当前线程,当前线程会把数据读取到应用程序用户空间。
在没有数据达到前,当前线程不会阻塞,因此叫非阻塞式IO,而有数据达到时,数据读取的工作是当前线程自己处理的(异步IO不需要线程自己读取数据),因此又叫同步IO,合在一起就是同步非阻塞IO。
这种IO机制虽然不会阻塞当前线程,但是不停尝试读取的做法非常消耗CPU资源,于是就有了IO多路复用。
IO多路复用机制
IO多路复用最大的特点就是一个线程可以监听多个socket。在Linux操作系统里面,提供了select、poll、epoll三种IO多路复用API,由于Redis底层使用的时epoll,我们就分析一下epoll这种IO多路复用机制。
epoll这种IO多路复用机制有三个函数,分别是epoll_create、epoll_ctl、epoll_wait。epoll_create的作用是创建一个epoll实例,这个epoll实例是一个IO多路复用器,里面使用一个红黑树结构存储注册进来的socket文件描述符,当某个socket有数据到达或有连接需要建立时,又会把该socket复制到一个链表结构当中;epoll_ctl则是把一个socket文件描述符注册到epoll实例当中;epoll_wait则是获取epoll实例中已经有事件就绪的socket,也就是epoll实例中的链表,把该链表拷贝到用户空间,当前线程就可以遍历该链表进行处理。
如果是建立连接事件,则调用socket的accept()函数建立连接获取另一个socket,把该socket注册到epoll实例中,如果是有数据达到,则调用socket的read()函数读取数据,如果是有数据需要写出,则调用socket的write()函数。
Redis的IO模型
redis基于IO多路复用进行封装,就有了自己的IO模型。Redis的IO模型是单线程Reactor模型,也就是事件监听(reactor)、建立建立连接(acceptor)、事件处理(handler),都由同一个线程负责。这里的reactor、acceptor、handler是IO模型中的三种角色,三个角色可以由不同线程担当,也可由同一个线程担当,在单线程Reactor这种IO模型中,显然三种角色都是由同一个线程担当。此时reactor表示事件监听的处理逻辑,acceptor表示连接建立的处理逻辑,handler表示处理读写事件的逻辑。
在单线程reactor这种IO模型下,程序会启动一个主线程,主线程启动时,会调用epoll的epoll_create函数创建一个epoll实例,并创建一个socket,调用listen函数把它转成监听socket,调epoll_ctl函数,注册到epoll实例中。然后主线程会调用epoll中的epoll_wait函数,监听注册到epoll实例中的所有socket,一旦有事件就绪就会进行事件分派,分派给acceptor或handler处理,这就是reactor的逻辑,
当注册到epoll实例中的socket有事件就绪,epoll_wait函数就会返回,当前线程就会遍历有事件就绪的socket,根据事件类型进行事件分派。如果是建立连接事件,就会调用acceptor的逻辑处理,acceptor中的逻辑就是调用socket.accept()获取已建立连接的socket,然后调用epoll_ctl把该socket注册到epoll实例;如果是读写事件,就会调用handler的逻辑处理读写事件,读事件的处理就是调用socket.read()获取数据然后进行相应处理,写事件就是调用socket.write()把数据写出。
Redis的事件驱动框架
基于这种单线程reactor的IO模型,redis就封装出了自己的事件驱动框架。
整个Redis事件驱动框架的主体逻辑就是一个主线程的死循环,在循环中,当前线程调用epoll_wait进行监听,获取有事件就绪的socket,然后放入一个队列,所有socket都放入队列后,会遍历队列中的socket进行事件分派。如果是连接建立事件,就会调用socket.accept()建立连接,然后调用epoll_ctl函数把返回的socket注册到epoll实例中;如果是读就绪事件,就调用socket.read()函数读取客户端发送的数据,也就是客户端发来的redis命令,然后执行该redis命令对应的函数;如果是写就绪事件,则调用socket.write()函数把数据写出。
Redis的核心逻辑是单线程处理,但不表示Redis真的就只有一个线程,一些文件关闭、惰性删除、AOF文件刷盘、执行bgsave命令写内存快照等任务,还是由后台线程来执行,所以Redis并不是真正的单线程。
在Redis的6.0版本开始,引入了多线程IO读写机制,此时Redis的事件驱动框架,在处理IO读写时会使用多线程的方式进行处理,而核心逻辑(也就是redis的命令处理)还是由一个主线程执行。
Redis会把所有读就绪的socket以轮询的方式分配给所有IO线程处理,IO线程会读取socket中的数据,然后主线程等待所有的IO线程读取完毕,再进行命令处理。处理完毕后,需要把处理结果写回客户端时,Redis再次进行分配,把待写回数据的socket分配给IO线程进行数据写回。
这样,即保持了Redis单线程处理的核心逻辑不变,又通过多线程IO读写这种机制,提升了IO读写的速度,从而进一步提升Redis的性能。