redis真的是单线程吗?
首先我们得厘清一个事实,我们说的Redis单线程,其实指的是redis的网络IO和键值对是由一个线程执行,这是redis对外提供键值存储服务的主要流程。此外,还有持久化、异步删除、主从复制数据同步等都是主线程fock出的子线程来执行的。但是fock子线程,主线程也是会阻塞的,实例越大,fock阻塞的时间就会越长。
所以严格来说,Redis并不是单线程来完成所有操作的。
redis为什么用单线程?
以redis中的list为例,若有线程1为List做LPUSH操作将list的长度+1,线程2做LPOP操作将list的长度-1,那么为了确保List长度的正确性,就需要将两个线程串行来完成,否则就可能导致取得的list的长度是错误的。这就是多线程模式下的资源共享的并发访问控制问题。
并发访问控制永远都是多线程不可避免的痛,若是没有优秀的系统设计,只是在并发过程中,增加了粗粒度互斥锁,那么即使使用了多线程,那每个线程访问都会等待锁,并行就变为了串行,并没有像理想中的那样提高吞吐率。
为了避免这个问题,redis干脆就直接使用了单线程模式。
但是呢,redis6.0中也提出了多线程设计模式,这其中是采用CPU多核多线程读写客户端操作,从而进一步提高服务的性能,当然这只是针对客户端的读写,而操作命令实际上还是由单线程来操作。
redis单线程为什么会这么快呢?
- redis的所有操作都在内存上完成,再加上它有高效的数据结构。
- redis采用的是多路复用机制,能使网络I/O流并发处理大量的客户端请求,提高吞吐率。
多路复用机制
接下来就要重点理解了。
基本的网络I/O模型包含了客户端与服务端的连接(accept)、监听连接(listen\bind)、获取客户端请求(recv)、解析客户端的请求(parse)、根据请求命令进行操作(get)、返回数据(send)。
这其中,accept、recv是可能存在的阻塞点。当服务端监听到客户端连接,但是迟迟没有连接成功,accept函数就会阻塞,就导致其他客户端无法与服务端进行连接。而若服务端迟迟没有从客户端中读取出具,那么recv也会被阻塞。同样的肯定存在解决方案,那就是网络模型可以设置非阻塞模式。
非阻塞模式
套接字 | 返回套接字类型 | 非阻塞模式 | 效果 |
---|---|---|---|
socket | 主动套接字 | ||
listen | 监听套接字 | √ | accept()非阻塞 |
accept | 已连接套接字 | √ | recv()、send()非阻塞 |
设置非阻塞模式后,如果redis线程调用accept但是迟迟没有响应,redis线程还可以执行其他操作,线程不会阻塞。虽然不用阻塞等待,但是还是需要机制监听accept并在有数据请求的时候通知redis,要注意的是调用accept()时已经存在监听套接字。
在Linux中,多路复用就是指在一个线程中要同时处理多个I/O流,就是select/epoll机制。基于多路复用机制,在redis中只启动单线程的情况下,该机制允许内核中存在多个监听套接字和多个已连接套接字
,由内核一直监听这些套接字,如果有请求到达,则提示通知redis。
为了能收到请求后通知redis,select/epoll提供了基于事件的回调函数
。针对不同事件的发生,调用不同处理函数。
如上图,每个RQ都是之前所说的套接字,在监听到套接字后,触发对应的事件,再将事件放入到事件队列中,redis单线程会一直不断的处理事件队列。在处理事件队列的时候,会根据事件调用对应的处理函数。因为redis会不断的处理事件队列,所以能及时的响应客户端请求,从而提高响应性能。