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


而且 , 采用多线程开发一般会引入同步原语来保护共享资源的并发访问 , 这也会降低系统代码的易调试性和可维护性 。 为了避免这些问题 , Redis直接采用了单线程模式 。
讲到这里 , 你应该已经明白了“Redis为什么用单线程” , 那么 , 接下来 , 我们就来看看 , 为什么单线程Redis能获得高性能 。
单线程Redis为什么那么快?
通常来说 , 单线程的处理能力要比多线程差很多 , 但是Redis却能使用单线程模型达到每秒数十万级别的处理能力 , 这是为什么呢?其实 , 这是Redis多方面设计选择的一个综合结果 。
一方面 , Redis的大部分操作在内存上完成 , 再加上它采用了高效的数据结构 , 例如哈希表和跳表 , 这是它实现高性能的一个重要原因 。 另一方面 , 就是Redis采用了多路复用机制 , 使其在网络IO操作中能并发处理大量的客户端请求 , 实现高吞吐率 。 接下来 , 我们就重点学习下多路复用机制 。
首先 , 我们要弄明白网络操作的基本IO模型和潜在的阻塞点 。 毕竟 , Redis采用单线程进行IO , 如果线程被阻塞了 , 就无法进行多路复用了 。
基本IO模型与阻塞点
你还记得我在第一节课介绍的具有网络框架的SimpleKV吗?
以Get请求为例 , SimpleKV为了处理一个Get请求 , 需要监听客户端请求(bind/listen) , 和客户端建立连接(accept) , 从socket中读取请求(recv) , 解析客户端发送请求(parse) , 根据请求类型读取键值数据(get) , 最后给客户端返回结果 , 即向socket中写回数据(send) 。
下图显示了这一过程 , 其中 , bind/listen、accept、recv、parse和send属于网络IO处理 , 而get属于键值数据操作 。 既然Redis是单线程 , 那么 , 最基本的一种实现是在一个线程中依次执行上面说的这些操作 。
高性能IO模型:为什么单线程Redis能那么快?
文章图片
但是 , 在这里的网络IO操作中 , 有潜在的阻塞点 , 分别是accept()和recv() 。 当Redis监听到一个客户端有连接请求 , 但一直未能成功建立起连接时 , 会阻塞在accept()函数这里 , 导致其他客户端无法和Redis建立连接 。 类似的 , 当Redis通过recv()从一个客户端读取数据时 , 如果数据一直没有到达 , Redis也会一直阻塞在recv() 。
这就导致Redis整个线程阻塞 , 无法处理其他客户端请求 , 效率很低 。 不过 , 幸运的是 , socket网络模型本身支持非阻塞模式 。
非阻塞模式
Socket网络模型的非阻塞模式设置 , 主要体现在三个关键的函数调用上 , 如果想要使用socket非阻塞模式 , 就必须要了解这三个函数的调用返回类型和设置模式 。 接下来 , 我们就重点学习下它们 。
在socket模型中 , 不同操作调用后会返回不同的套接字类型 。 socket()方法会返回主动套接字 , 然后调用listen()方法 , 将主动套接字转化为监听套接字 , 此时 , 可以监听来自客户端的连接请求 。 最后 , 调用accept()方法接收到达的客户端连接 , 并返回已连接套接字 。
高性能IO模型:为什么单线程Redis能那么快?
文章图片
针对监听套接字 , 我们可以设置非阻塞模式:当Redis调用accept()但一直未有连接请求到达时 , Redis线程可以返回处理其他操作 , 而不用一直等待 。 但是 , 你要注意的是 , 调用accept()时 , 已经存在监听套接字了 。
虽然Redis线程可以不用继续等待 , 但是总得有机制继续在监听套接字上等待后续连接请求 , 并在有请求时通知Redis 。
类似的 , 我们也可以针对已连接套接字设置非阻塞模式:Redis调用recv()后 , 如果已连接套接字上一直没有数据到达 , Redis线程同样可以返回处理其他操作 。 我们也需要有机制继续监听该已连接套接字 , 并在有数据达到时通知Redis 。