Redis的单线程主要是指Redis的网络IO和键值对读写是由一个线程完成的,Redis在处理客户端的请求时包括获取(Socket读)、解析、执行、内容返回(Socket写)等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。这也是Redis对外提供键值存储服务的主要流程。
但Redis的其他功能,比如持久化RDB、AOF、异步删除、集群数据同步等等,其实是由额外的线程执行的。
从redis6.x开始采用多线程,让多个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),将最耗时的Socket的读取、请求解析、写入外包出去,剩下的命令执行仍然由主线程串行执行并和内存的数据交互。
也就是说就新增了多线程的功能来提高 I/O 的读写性能,使多个 socket 的读写可以并行化。
1.基于内存;
2.简单的数据结构;
3.单线程,避免上下文切换;
4.IO多路复用。
一种同步的IO模型,实现一个线程监视多个文件句柄,一旦某个文件句柄就绪就能够通知到对应应用程序进行相应的读写操作,没有文件句柄就绪时,就会阻塞应用程序,从而释放cpu资源。一个专家大夫和多个病人之间的关系。
Io多路复用:单个进程能够实现处理多个客户端的连接请求。或者一个服务进程能够可以同时处理多个套接字描述符。
epoll的原理:将用户socket对应的文件描述符注册进epoll,然后epoll帮你监听哪些socket上有消息到达。此时的socket应该采用非阻塞模式。这样,整个过程只有在调用select、poll、epoll这些调用的时候才会阻塞,收发客户信息是不会阻塞的,整个进程或者线程就被充分利用起来。这就是事件驱动,所谓的Reactor反应模式。
Redis基于Reactor模式开发了自己的网络事件处理器-文件事件处理器(fiel event handler, FEH),该处理器是单线程的,所以redis就是单线程模式。
采用I/O多路复用同时监听多个socket,根据socket当前执行的事件来为 socket 选择对应的事件处理器。
当被监听的socket准备好执行accept、read、write、close等操作时,和操作对应的文件事件就会产生,这时FEH就会调用socket之前关联好的事件处理器来处理对应事件。
所以虽然FEH是单线程运行,但通过I/O多路复用监听多个socket,不仅实现高性能的网络通信模型,又能和Redis 服务器中其它同样单线程运行的模块交互,保证了Redis内部单线程模型的简洁设计。
下面来看文件事件处理器的几个组成部分。
I/O 多路复用程序会将所有产生事件的socket放入队列, 通过该队列以有序、同步且每次一个socket的方式向文件事件分派器传送socket。
文件事件分派器接收IO多路复用程序传来的socket,并根据socket产生的事件类型,调用相应的事件处理器。
服务器会为执行不同任务的套接字关联不同的事件处理器, 这些处理器是一个个函数, 它们定义了某个事件发生时, 服务器应该执行的动作。
Redis 为各种文件事件需求编写了多个处理器,若客户端连接Redis,对连接服务器的各个客户端进行应答,就需要将socket映射到连接应答处理器,写数据到Redis,接收客户端传来的命令请求,就需要映射到命令请求处理器,从Redis读数据,向客户端返回命令的执行结果,就需要映射到命令回复处理器,当主服务器和从服务器进行复制操作时, 主从服务器都需要映射到特别为复制功能编写的复制处理器。
服务器端的Reactor是一个线程对象,该线程会启动事件循环,并使用Acceptor事件处理器关注ACCEPT事件,这样Reactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。
客户端向服务器端发起一个连接请求,Reactor监听到了该ACCEPT事件的发生并将该ACCEPT事件派发给相应的Acceptor处理器来进行处理。建立连接后关注的READ事件,这样一来Reactor就会监听该连接的READ事件了。
当Reactor监听到有读READ事件发生时,将相关的事件派发给对应的处理器进行处理。比如,读处理器会通过读取数据,此时read()操作可以直接读取到数据,而不会堵塞与等待可读的数据到来。
在目前的单线程Reactor模式中,不仅I/O操作在该Reactor线程上,连非I/O的业务操作也在该线程上进行处理了,这可能会大大延迟I/O请求的响应。所以我们应该将非I/O的业务逻辑操作从Reactor线程上卸载,以此来加速Reactor线程对I/O请求的响应。
从Redis自身角度来说,因为读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶
颈主要在于网络的 IO 消耗,多线程任务可以分摊 Redis 同步 IO 读写负荷。
流程简述如下:
1、主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列
2、主线程处理完读事件之后,通过 RR(Round Robin) 将这些连接分配给这些 IO 线程
3、主线程阻塞等待 IO 线程读取 socket 完毕
4、主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行回写 socket
5、主线程阻塞等待 IO 线程将数据回写 socket 完毕
6、解除绑定,清空等待队列
该设计有如下特点:
1、IO 线程要么同时在读 socket,要么同时在写,不会同时读或写
2、IO 线程只负责读写 socket 解析命令,不负责命令处理
注意:Redis的多线程不存在线程并发安全问题。Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。