前言
Redis 是一个具有多种数据结构,基于内存的数据库,对数据的操作都是在内存中完成的,因此读写速度非常快,非常适合用于缓存、分布式锁等场景。
我们都知道 Redis 处理请求是单线程的,但是它的吞吐量却可以达到 10W/S,除了基于内存操作之外,还有一个很重要的特性,就是 Redis 基于事件的驱动模型。
IO 多路复用
IO 多路复用是操作系统提供给用户使用的一种网络连接处理方式。它可以通过一个线程处理大量客户端的 Socket 请求。
在介绍 IO 多路复用之前,我们先来看看其余的几种网络处理模型。
单线程网络处理模型
我们使用单线程来处理网络连接。当有连接进来时,线程会被唤醒处理请求,在进行读写操作时,线程会被阻塞,直到有数据可读可写,期间线程不能做任何事。
举个例子:我们去银行办事,柜台中只有一个工作人员,每次办事都需要填写表格。当第一个人来到的时候,在填写表格期间,后面又有人来到了,但是服务人员只能等第一个人填好表格,并服务完成之后,才能继续服务第二个人。
多线程网络处理模型
在单线程网络模型中,因为只有一个线程在处理请求,一旦陷入阻塞状态,后面的请求就会无法及时处理。那么我们可以来一个请求就创建一个线程,用创建的子线程去处理请求。任务处理完成后,我们再将线程释放。
举个例子:每来一个客户,我们就新招一个工作人员,然后让这个工作人员去处理新客户的业务。这样的话,不管来多少个客户,我们都可以同时去处理这些客户的业务,处理完之后,我们再将这些人解雇。(有点像外包)
但是这个模型有很多缺点:
- 银行的资源是有限的,不可能一直招人。当发现资源不足,无法再继续招人时,银行就破产了。
- 招人和解雇是需要花费时间和资源的,成本较高。在招人期间也无法服务客户。
线程池网络处理模型
在多线程模型中,我们每次招人做完事情之后就将它解雇了,在招人和解雇都需要花费大量的时间,成本很高。那么我们是否可以先招几个员工,然后就一直用这几个员工进行服务就可以了。对应起来就是,先创建多个线程,然后循环使用这些线程去处理网络请求。而这些线程组合起来就叫线程池。
EPOLL
Epoll 是 Linux 上对 IO 多路复用的一种实现,Mac OS 上是 Kqueue,它们本质上都是 IO 多路复用的不同实现方式。我们今天就拿 Epoll 作为例子进行讲解。
Epoll 是一个事件驱动的 IO 多路复用。
比如说:我去服装店定制一套西装,但是我不知道什么时候制作完成,我就跟老板说,制作完成之后打电话告诉我。西装制作完成是我关心的事件,老板就是事件通知器,当这个事件完成之后,老板就会打电话通知我。
在 Epoll 中,Epoll 就是事件通知器,可以向 Epoll 注册我们感兴趣的事件。
在服务端,启动一个服务器需要指定端口,我们会监听这个指定的端口。当监听这个端口的时候会产生一个 sfd,这个 sfd 就是网络监听的文件描述符。我们就可以将这个 sfd 注册进 Epoll,并告诉 Epoll,如果这个 sfd 发生了可读事件之后,来通知我。
客户端连接服务端的这个端口,Epoll 会检测到这个 sfd 发生了事件,并且在缓冲区可读的情况下,通知用户线程事件发生。我们就知道,sfd 发生了一个网络连接事件,我们就可以接收这个网络连接。
接收网络连接时,又会产生一个 cfd,表示客户端与服务器建立连接的文件描述符。我们可以将 cfd 注册可读事件到 epoll 中,当客户端发送的数据到达缓冲区时,Epoll 就会通知我们可读,我们就可以去读取数据。
与上面网络模型的不同之处在于:Epoll 会在缓存区有数据可读的情况才会通知用户线程,而在此期间,用户线程可以去做其他事件。这就避免造成用户线程读取数据,数据还未到达而无法做其他事情的情况。
Redis 事件循环
在 Redis 中,底层就是使用 IO 多路复用处理网络请求。并且创建一个 EventLoop 对象专门处理事件。
上图是 EventLoop 与 Epoll 的对应关系。往 Epoll 注册指定文件描述符的事件之前,会在 EventLoop 上记录该文件描述符监听的事件和事件通知时需要执行的函数。
当 Epoll 发现 sofd 上有可读事件发生,会通知给用户线程。用户线程可以拿到 sofd 在 EventLoop 上找到相应的记录,拿到记录上的函数,执行该函数。
启动事件循环
服务器启动
在服务端启动的时候,会绑定端口,并监听端口是否有连接进来。这个过程会完成以下几个步骤:
- 监听端口之后,会返回一个 sofd,表示这个监听端口的文件描述符
- 使用文件描述符在 EventLoop 中注册事件,并设置回调函数为
tcpAcceptHandler
。当有事件发生时,会调用这个函数。 - 使用文件描述符注册可读事件到 Epoll 中。
客户端连接
客户端要访问服务器,首先要跟客户端进行连接,使用 TCP 与服务器建立连接。这个过程会完成以下几个步骤
-
客户端与服务器完成 TCP 三次握手之后,Epoll 会检测到 sofd 上发生了可读事件。
-
Epoll 会通知用户线程,该 sofd 发生了可读事件。
-
用户线程接收到该事件后,会通过 sofd 从EventLoop 中找到对应注册的事件,然后执行事件中的函数 tcpAcceptHandler 处理 TCP 连接。
-
tcpAcceptHandler 执行过程:
- 通过 accept 函数接收连接,并返回 cfd。
- 使用 cfd 在 EventLoop 中注册读事件,并设置回调函数为 readQueryFromClient。
- 使用 cfd 在 Epoll 中注册读事件。
执行完这个过程后,就完成了客户端连接,并等待客户端发送请求。
客户端发送请求
客户端连接上服务器之后,接着就要发送请求,让服务器执行命令,返回数据。这个过程会完成以下几个步骤:
-
客户端通过连接发送请求,数据到达缓冲区。
-
Epoll 检测到缓冲区有数据,通知用户线程 cfd 发生了可读事件。
-
用户线程通过 cfd 从 EventLoop 中拿到对应注册的事件,然后执行 readQueryFromClient 函数,处理此次请求。
-
readQueryFromClient
- 调用 read 系统函数读取数据
- 解析命令
- 执行命令
- 返回响应
总结
Redis 通过 IO 多路复用技术,使用单线程就可以获取很高的并发访问。同时通过事件循环,将对应的功能都分散到不同的函数中,实现了高内聚。