redis线程io模型

110 阅读4分钟

一个应用程序, 想对外提供服务, 一般都是通过建立套接字监听端口来实现, 也就是socket,在这个时候, 应用对外提供服务的过程大概是这样:

  1. 创建套接字
  2. 绑定端口号
  3. 开始监听
  4. 当监听到连接时, 调用系统read去读取内容, 但是读取操作是阻塞的(也就是说,如果主线程处理read,就不能接收其他连接了, 所以只能开新的线程去处理这个事情 这个流程的问题很明显, 会不停的创建线程, 当然, 可以维护一个线程池。但是线程之间的不停切换也是消耗资源的, 而且也不可能无限的创建线程。那我如果想一个线程处理呢? 问题出在阻塞上面. 如果read操作可以立刻返回结果, 如果没有读到数据, 就可以继续处理后边的事情了。

版一

整个简单版本. 主线程维护一个所有连接的列表, 每次循环读取所有列表, 有数据就处理, 没有就跳过.

  1. 创建套接字
  2. 绑定端口号
  3. 开始监听
  4. 监听到连接, 将连接加到连接列表中, 循环读取连接列表中的所有连接, 对有数据的进行处理 现在这样处理貌似是比开线程要好一些了, 但是事实是这样么? 众所周知, 其中的read操作是调用系统函数, 简单说就是要进行进程的切换, 从用户进程切换到系统进程. 连接少还好, 如果有十万个连接, 甚至更多呢? 每次循环都会频繁的调用系统函数, 可能十万次调用, 甚至其中有数据的只有一次, 其余调用都白掉了. 这无疑降低了性能.

其实解决方法说起来也很容易想到. 问题出在系统调用上, 频繁的系统调用导致了问题的出现. 如果能够一次性将需要查询的所有数据都发给系统, 让系统进行查询, 那不就只需要一次切换就可以了么?

select版本

为了解决上面的问题. 可以批量将待查询的连接发给系统. 出现了select, 这就是说的 多路复用 了. 在系统 中, 无论是监听端口还是建立了连接, 程序拿到的都是一个文件描述符, 将这些文件描述符批量查询就是了. 这里和上一个版本相比, 将循环检查交到了系统去做, 只发生一次进程的切换。但是这里系统也需要去遍历。

非阻塞IO有个问题,那就是线程要读数据,结果读了一部分就返回了,那么线程如何知道何时才应该继续读,也就是说,当数据到来时,线程如何得到通知。写也是一样,如果缓冲区满了,写不完,剩下的数据何时才应该继续写,线程也应该得到通知。事件轮询API 就是用来解决这个问题的。最简单的事件轮询API 是select函数,它是操作系统提供给用户程序的API 。服务器套接字serversocket 对象的读操作是指调用accept接受客户端新连接。何时有新连接到来,也是通过select 系统调用的读事件来得到通知的。

epoll

epoll是将你需要监听的列表交给系统维护, 这样当有新数据来的时候, 系统知道这是你要的, 等你下次来拿的时候, 直接给你了, 少去了上面的系统遍历. 同时, 也没有select查询时那一大堆参数, 每次都只调用一次进行绑定即可.

指令队列

Redis 会将每个客户端套接字都关联个指令队列。客户端的指令通过队列来排 队进行顺序处理,先到先服务。

响应队列

Redis 同样也会为每个客户端套接字关联一个晌应队列。Redis服务器通过响应队列来将指令的返回结果回复给客户端。如果队列为空,那么意昧着连接暂时处于空闲状态,不需要去获取写事件。等到队列有数据了,再将描述符放进去,避免select 系统调用立即返回写事件,结果发现没什么数据可以写,出现这种情况的线程会令CPU 消耗飘升。

redis6.0多线程

流程如下;

  • 主线程负责接收建立连接请求,获取 Socket 放入全局等待读处理队列。
  • 主线程处理完读事件之后,通过 RR(Round Robin)将这些连接分配给这些 IO 线程。
  • 主线程阻塞等待 IO 线程读取 Socket 完毕。
  • 主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行。
  • 主线程阻塞等待 IO 线程将数据回写 Socket 完毕。 引入多线程目的是提高io效率。