首先我们知道Redis是采用的IO多路复用技术,还有其他的几种这种这里就不介绍了。本文主要将跟Redis有关的 首先让我们先了解一下什么是IO多路复用
# IO多路复用
文件描述符(File Descriptor):简称FD,是一个从0 开始的无符号整数,用来关联Linux中的一个文件。在Linux中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket)。
IO多路复用:是利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。
阶段一:
- 用户进程调用select,指定要监听的FD集合
- 内核监听FD对应的多个socket
- 任意一个或多个socket数据就绪则返回readable
- 此过程中用户进程阻塞
阶段二:
- 用户进程找到就绪的socket
- 依次调用recvfrom读取数据
- 内核将数据拷贝到用户空间
- 用户进程处理数据
IO多路复用是利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源,不过监听FD的方式、通知的方式又有多种实现,常见的有:select、poll、epoll。
差异:
- select和poll只会通知用户进程有FD就绪,但不确定具体是哪个FD,需要用户进程逐个遍历FD来确认
- epoll则会在通知用户进程FD就绪的同时,把已就绪的FD写入用户空间
epoll模式是对select和poll的改进,它提供了三个函数
其实简单来说epoll模式首先将select、poll模式当中的select分成了俩步(阻塞IO和非阻塞IO select的左右就俩个把FD从用户空间拷贝到内核空间,然后等待FD数据就绪),
- 通过epoll_create创建出epoll实例,epoll模式中当额poll实例被创建后,会创建出rb_root红黑树,用来保存要监听的FD,和list_nead链表,用户存储就绪的FD。
- epoll_ctl。这个函数的作用是监听FD,也就是将FD从用户空间拷贝到内核空间,epoll_wait是等待FD就绪。将来如果有新的FD,执行一次ctl加入就可以了。并且一直存在,
- 以后循环的时候只需要循环epoll_wait就好了。不需要每次都拷贝到内核空间。当FD就绪后就会加入到list_head中并且返回给用户空间的只有已经就绪的FD,而不是返回所有
总结:
select模式存在的三个问题:
- 能监听的FD最大不超过1024
- 每次select都需要把所有要监听的FD都拷贝到内核空间
- 每次都要遍历所有FD来判断就绪状态
poll模式的问题:
- poll利用链表解决了select中监听FD上限的问题,但依然要遍历所有FD,如果监听较多,性能会下降
epoll模式中如何解决这些问题的?
- 基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高
- 每个FD只需要执行一次epoll_ctl添加到红黑树,
- 以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间利用
- ep_poll_callback机制来监听FD状态,无需遍历所有FD,因此性能不会随监听的FD数量增多而下降
# Redis网络模型
Redis到底是单线程还是多线程?
- Redis的核心业务部分(命令处理)--单线程
- 整个Redis--多线程
Redis v4.0:引入多线程异步处理一些耗时较旧的任务,例如异步删除命令unlinkRedis v6.0:在核心网络模型中引入 多线程,进一步提高对于多核CPU的利用率。
因此,对于Redis的核心网络模型,在Redis 6.0之前确实都是单线程。是利用epoll(Linux系统)这样的IO多路复用技术在事件循环中不断处理客户端情况。
为什么Redis要选择单线程
- 抛开持久化不谈,Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升
- 多线程会导致过多的上下文切换,带来不必要的开销
- 引入多线程面临线程安全问题,必要要引入线程锁这样的安全手段,实现复杂度高,而且性能会大打折扣。
redis单线程网络模型的整个流程
- 首先当Redis启动时会先创建一个serverSocket,当然serverScoket创建完会有一个fd,并且注册到aeEventLoop上,类似于epoll实例。注册以后还要绑定一个处理器,因为serverSocket可读,所以它需要一个处理器去处理,名字叫tcpAccepthadler,这个处理器是专门来处理serverSocket的可读事件的,一旦serverSocket可读,tcpAccepthadler这个处理器就会被调用。
- 那么serverSocket什么时候可读,就是当客户端连接上来的时候,就会不断触发serverSocket上的读事件,serverSokcet的事件一触发,就会调用对应的处理器。这个处理器会去接收客户端的请求,得到clientsocket的fd,然后又一次把它注册到aeEventLoop上,那么这个aeEventLoop就会有多个serverSocket和clientSocket,然后继续执行aeApiPoll等待,那么这次等待到的读事件就可能不是一个了。可能是serverSocket也可能是clientSocket,
- 当客户端Socket可读的时候又会绑定一个处理器---readQueryFormClient。readQueryFormClient函数里面首先他要先给每个客户端封装一个对应的client,client里面封装一个queryBuf,代表查询缓冲区,接下来他会把请求读一下放到querybuf里。然后解析queryBuf数据为Redis命令。接下来就开始处理这些命令,从redis中的command中找到这些命令,然后就真正的去操作对应的数据了,当数据操作完成后,会把数据写到输出缓冲区中(buf或者reply,buf类似于一个数据,reply是一个链表,当buf写不下时就会写到reply中去)此时已经写到客户端缓冲区了,还没有写到客户端。这个时候会创建一个队列(server. clients_pending_write),存放那些被写的客户端。这个时候readQueryFormClient这个处理器就结束了。
- 这个时候如果beforeSleep这个函数执行,那么他就会去队列里面去取等待被写出的客户端。这个时候会绑定sendReplyToClient处理器,这个是写处理器 ,将数据写回到clientSocket。
其实avEventLoop、beforesleep、aeApiPolll整体可以看着一个IO多路复用+事件派发。其实整个流程可以看做是不同的监听,包括监听serverSocket,clientSocket,事件有三种 serverSocket可读,clientSocket可读、可写,监听到不同事件派发给相应的处理器当中。
Redis 6.0版本中引入了多线程,目的是为了提高IO读写效率。因此在解析客户端命令、写响应结果时采用了多线程。核心的命令执行、IO多路复用模块依然是由主线程执行。