众所周知,redis是单线程的。但是官方提供的压测数据,redis每秒可以支持10万的QPS,这个数据很惊人。但是redis的单线程不是说redis内部就只有一条线程。而是说redis处理请求的线程是单线程。像刷盘这种操作,有另外的线程去做的。
在计算机多cpu多核心的时代,redis为什么要设计成单线程?有以下几个理由
1).做过多线程开发的朋友都知道,如果不考虑业务场景,不考虑cpu的实际情况,肆意增大线程,会导致线程上下文切换非常的严重。单线程就完全不用理会上下文切换的消耗。
2).redis执行一个客户端指令,只需要大概1微秒的时间。所以理论上,1秒钟的时间,redis可以执行100万个客户端指令,大概几十万次请求。这种处理能力,完全不需要多线程。所以redis的作者设计之初就考虑到,redis的性能瓶颈不在cpu,在网络资源
redis能支持如此之高的并发量,其原因有二:
1 .纯内存操作
2 .IO多路复用
1 .多路复用
关于IO多路复用,涉及到两个词。多路和复用
多路:多路socket连接
复用:多个socket连接使用的是一个线程
redis的多路复用依赖的是事件轮询机制。
事件轮询机制有几个API,select、epoll、kqueue、evport。
在linux系统上,redis采用的是epoll函数。
在macos、FreeBSD系统上采用的是kqueue函数
在solaries系统上,redis采用的是evport函数
如果以上函数,操作系统均不支持,此时采用select函数。select在所有操作系统上都会存在。
以上描述,使得redis做到了跨平台性
此处,我们只介绍一下select函数,其实本质上和其他几个API差不多
当客户端socket发起读写请求时,redis服务端程序调用系统内核的select函数,传入当前socket的文件读写描述符以及一个超时时间timeout。select函数会一直盯着这些文件描述符,当文件描述符处于可读/可写的状态时。select函数会将该事件立即返回,这就可以顺序执行所有已经就绪的socket,省了很多无用的操作。另外,当timeout到达后,select函数也会立即返回,不会无休止的阻塞下去。客户端的整个请求链路,只有在调用select函数时阻塞,其他时候不阻塞。通过select函数,redis在接受客户端请求时,不会阻塞主线程。
select函数伪代码如下:
read_events,write_events = select (read_fds,write_fds,timeout)
for event in read_events:
handle_read(event.fd)
for event in write_events:
handle_write(event.fd)
handle_others( ) #处理其他事情,如定时任务等
由上可以看出,select返回的是一系列的事件,然后循环处理。此时就达到了复用线程的目的。
拿到事件后,redis的那条单线程就会开始处理这个事件,处理完后。继续过来轮询,于是线程进入了一个死循环,我们把这个死循环称为时间循环。一个循环为一个周期
2 .事件分派器
IO多路复用模块会将已经处于读写就绪状态的事件压入队列。然后事件分派器从队列中挨个取出事件,根据事件的不同类型分配不同的事件处理器(连接应答处理器,命令请求处理器,命令回复处理器)。事件分派器自己不干活,交给别人干活
这个事件分派器的工作模式有一个专有的名词,叫做Reactor模式。和tomcat的处理连接的过程是非常相似的,tomcat内部也采用了Reactor模式
3 .redis工作流程图
原图出处:https://zhuanlan.zhihu.com/p/65013389
4 .完整的redis请求
我们把以上的知识点,串起来说一下总的流程
客户端socket发起对redis服务端的连接请求,将自己的文件描述符提交给redis的多路复用模块。多路复用模块监听该描述符的状态,一旦该描述符就绪,就会返回描述符对应的事件。然后多路复用模块将事件压入一个队列中。事件分派器从队列中取出事件交给事件处理器进行处理,然后将执行结果返回给socket客户端。
以上,是自己的一点琢磨。应该会有描述欠妥的地方,希望路过的大神能给予批评指正,谢谢!
redis的线程模型
释放双眼,带上耳机,听听看~!