前言
redis是单线程的(不严谨的讲法的哈),为什么还这么快,很多人相信会回答因为redis是基于内存操作的, 内存的读写速度是非常快的。答到这,逼格还是不够高的,基于内存是一方面,但还有一个关键点是:redis采用了多路复用技术,今天我们就来聊聊这个点。
什么是多路服用
- 多路:多个客户端连接
- 复用:使用单进程就能够实现同时处理多个客户端的连接
它的基本原理就是不再由应用程序自己监视连接,而是由内核替应用程序监视文件描述符。客户端在操作的时候,会产生具有不同事件类型的 socket。在服务端,I/O 多路复用程序(I/O Multiplexing Module)会把消息放入队列中,然后通过文件事件分派器(Fileevent Dispatcher),转发到不同的事件处理器中。
可以想象一条高速公路,车辆在不同的车道上行驶,但它们共用同一条路。
I/O多路复用的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符, 一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作
在Unix/Linux系统中,常用的IO多路复用机制有以下几种:
- select:是最早引入的IO多路复用机制,通过select函数来监视多个文件描述符的状态变化,但其缺点是性能不高,同时对文件描述符的数量有一定的限制。
- poll:是select的改进版本,没有了描述符数量的限制,但其性能仍然不是很高。
- epoll:是Linux特有的IO多路复用机制,是目前性能最好的IO多路复用技术。它使用了事件驱动的方式,通过epoll_ctl函数注册文件描述符的事件,然后通过epoll_wait函数等待IO事件的发生。epoll在处理大量连接时具有更好的扩展性和性能。
epoll 最大的优点就在于它只管你“活跃”的连接 ,而跟连接总数无关,因此在实际的网络环境
中, epoll 的效率就会远远高于 select 和 poll 。 (redis 的iO模型默认采用epoll实现的。)
IO多路复用的主要优点包括:
- 节省了线程/进程的创建和销毁开销,降低了系统资源的占用。
- 可以实现高并发处理,提高系统的性能和吞吐量。
- 编程模型相对简单,代码量较少,易于维护和管理。
select | poll | epoll | |
操作方式 | 遍历 | 遍历 | 回调 |
数据结构 | bitmap | 数组 | 红黑树 |
最大连接数 | 1024(x86)或者2048(x64) | 无上限 | 无上限 |
最大支持文件描述符数 | 有最大值限制 | 65535 | 65535 |
fd拷贝 | 每次调用,都需要把fd结合从用户态拷贝到内核态 | 每次调用,都需要把fd结合从用户态拷贝到内核态 | 只有首次调用的时候拷贝 |
工作效率 | 每次都要遍历所有文件描述符,时间复杂度O(n) | 每次都要遍历所有文件描述符,时间复杂度O(n) | 每次只用遍历需要遍历的文件描述符,时间复杂度O(1) |
Redis中的多路复用
Redis 是跑在单线程中,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以IO操作一般情况下往往不能直接返回,这会导致某一文件的I/O 阻塞导致整个进程无法对其他客户提供服务,I/O多路复用是为了解决这个问题而出现的。
Redis中的IO多路复用模式:
- (1)一个 socket 客户端与服务端连接时,会生成对应一个套接字描述符(套接字描述符是文件描述符的一种),每一个 socket 网络连接其实都对应一个文件描述符。
- (2)多个客户端与服务端连接时,Redis 使用 「I/O 多路复用程序」 将客户端 socket 对应的 FD 注册到监听列表(一个队列)中。当客服端执行 read、write 等操作命令时,I/O 多路复用程序会将命令封装成一个事件,并绑定到对应的 FD 上。
- (3)「文件事件处理器」使用 I/O 多路复用模块同时监控多个文件描述符(fd)的读写情况,当
accept
、read
、write
和close
文件事件产生时,文件事件处理器就会回调 FD 绑定的事件处理器进行处理相关命令操作。 - (4)整个文件事件处理器是在单线程上运行的,但是通过 I/O 多路复用模块的引入,实现了同时对多个 FD 读写的监控,当其中一个 client 端达到写或读的状态,文件事件处理器就马上执行,从而就不会出现 I/O 堵塞的问题,提高了网络通信的性能。
这种IO多路复用机制能够有效地减少系统调用和上下文切换的开销,提高了Redis的性能和并发能力。同时,由于采用了非阻塞IO模型,Redis能够处理大量的连接而不会造成线程堵塞,提高了系统的可伸缩性。
Redis单线程?多线程?
Redis 在处理客户端的请求时,包括获取 (socket 读)、解析、执⾏、内容返回 (socket 写) 等都由⼀个顺序串⾏的主线程处理,这就是所谓的「单线程」。
Redis是单线程来执行命令的,每一条到达读服务端的命令并不会立即执行,所有的命令都会进入一个 socket 任务队列中,当 socket 可读则交给单线程事件分发器逐个被执行,即一个线程处理所有网络请求
Redis 采⽤多个 IO 线程来处理⽹络请求,提⾼⽹络请求处理的并⾏度。Redis 多 IO 线程模型只⽤来处理处理网络数据的读写和协议解析,对于 Redis 的读写命令,依然是单线程处理。
因为网络 I/O 在 Redis 执行期间占用了大部分 CPU 时间, 所以把网络 I/O 部分单独抽离出来, 做成多线程的方式。这里所说的多线程, 其实就是将 Redis 单线程中做的这两件事情"从客户端读取数据、回写数据给客户端"(也可以称为网络 I/O), 处理成多线程的方式, 但是"执行 Redis 命令"还是在主线程中串行执行, 这个逻辑保持不变