高性能IO模型:为什么单线程Redis能那么快?( 四 )


这样才能保证Redis线程 , 既不会像基本IO模型中一直在阻塞点等待 , 也不会导致Redis无法处理实际到达的连接请求或数据 。
到此 , Linux中的IO多路复用机制就要登场了 。
基于多路复用的高性能I/O模型
Linux中的IO多路复用机制是指一个线程处理多个IO流 , 就是我们经常听到的select/epoll机制 。 简单来说 , 在Redis只运行单线程的情况下 , 该机制允许内核中 , 同时存在多个监听套接字和已连接套接字 。 内核会一直监听这些套接字上的连接请求或数据请求 。 一旦有请求到达 , 就会交给Redis线程处理 , 这就实现了一个Redis线程处理多个IO流的效果 。
下图就是基于多路复用的RedisIO模型 。 图中的多个FD就是刚才所说的多个套接字 。 Redis网络框架调用epoll机制 , 让内核监听这些套接字 。 此时 , Redis线程不会阻塞在某一个特定的监听或已连接套接字上 , 也就是说 , 不会阻塞在某一个特定的客户端请求处理上 。 正因为此 , Redis可以同时和多个客户端连接并处理请求 , 从而提升并发性 。
高性能IO模型:为什么单线程Redis能那么快?
文章图片
为了在请求到达时能通知到Redis线程 , select/epoll提供了基于事件的回调机制 , 即针对不同事件的发生 , 调用相应的处理函数 。
那么 , 回调机制是怎么工作的呢?其实 , select/epoll一旦监测到FD上有请求到达时 , 就会触发相应的事件 。
这些事件会被放进一个事件队列 , Redis单线程对该事件队列不断进行处理 。 这样一来 , Redis无需一直轮询是否有请求实际发生 , 这就可以避免造成CPU资源浪费 。 同时 , Redis在对事件队列中的事件进行处理时 , 会调用相应的处理函数 , 这就实现了基于事件的回调 。 因为Redis一直在对事件队列进行处理 , 所以能及时响应客户端请求 , 提升Redis的响应性能 。
为了方便你理解 , 我再以连接请求和读数据请求为例 , 具体解释一下 。
这两个请求分别对应Accept事件和Read事件 , Redis分别对这两个事件注册accept和get回调函数 。 当Linux内核监听到有连接请求或读数据请求时 , 就会触发Accept事件和Read事件 , 此时 , 内核就会回调Redis相应的accept和get函数进行处理 。
这就像病人去医院瞧病 。 在医生实际诊断前 , 每个病人(等同于请求)都需要先分诊、测体温、登记等 。 如果这些工作都由医生来完成 , 医生的工作效率就会很低 。 所以 , 医院都设置了分诊台 , 分诊台会一直处理这些诊断前的工作(类似于Linux内核监听请求) , 然后再转交给医生做实际诊断 。 这样即使一个医生(相当于Redis单线程) , 效率也能提升 。
不过 , 需要注意的是 , 即使你的应用场景中部署了不同的操作系统 , 多路复用机制也是适用的 。 因为这个机制的实现有很多种 , 既有基于Linux系统下的select和epoll实现 , 也有基于FreeBSD的kqueue实现 , 以及基于Solaris的evport实现 , 这样 , 你可以根据Redis实际运行的操作系统 , 选择相应的多路复用实现 。
小结
今天 , 我们重点学习了Redis线程的三个问题:“Redis真的只有单线程吗?”“为什么用单线程?”“单线程为什么这么快?”
现在 , 我们知道了 , Redis单线程是指它对网络IO和数据读写的操作采用了一个线程 , 而采用单线程的一个核心原因是避免多线程开发的并发控制问题 。 单线程的Redis也能获得高性能 , 跟多路复用的IO模型密切相关 , 因为这避免了accept()和send()/recv()潜在的网络IO操作阻塞点 。
搞懂了这些 , 你就走在了很多人的前面 。 如果你身边还有不清楚这几个问题的朋友 , 欢迎你分享给他/她 , 解决他们的困惑 。