Redis6。0多线程无锁(lockfree)设计和多线程R
干货:单线程模式并非CPU瓶颈多线程网络模型多线程Reactor模式多线程IOlockfree无锁模式
因为我们的主题是多线程,所以不会过多涉及单线程。1。单线程模式并非CPU瓶颈
咱们都知道单线程的程序是没法利用服务器的多核CPU的,那么早期的Redis为何还要使用单线程呢?咱们不妨先看一下Redis官方给出的回答:
核心意思是:CPU并非制约Redis性能表现的瓶颈所在,更多状况下是受到内存大小和网络IO的限制,因此Redis核心网络模型使用单线程并无什么问题,若是你想要使用服务的多核CPU,能够在一台服务器上启动多个实例或者采用分片集群的方式。
咱们知道Redis的IO线程除了在等待事件,其它的事件都是非阻塞的,没有浪费任何的CPU时间,这也是Redis可以提供高性能服务的缘由。2。多线程网络模型多线程Reactor模式
Redis在6。0版本以后正式在核心网络模型中引入了多线程,它的工做模式是这样的:
区别于单Reactor模式,这种模式再也不是单线程的事件循环,而是有多个线程(SubReactors)各自维护一个独立的事件循环,由MainReactor负责接收新链接并分发给SubReactors去独立处理,最后SubReactors回写响应给客户端。
MultipleReactors模式一般也能够等同于MasterWorkers模式,好比Nginx(前期文章有分享哈,可以回头去看)等就是采用这种多线程模型,虽然不一样的项目实现细节略有区别,但整体来讲模式是一致的。2。1多线程工作流程
Redis服务器启动,开启主线程事件循环(EventLoop),注册acceptTcpHandler链接应答处理器到用户配置的监听端口对应的文件描述符,等待新链接到来;客户端和服务端创建网络链接;acceptTcpHandler被调用,主线程使用AE的API将readQueryFromClient命令读取处理器绑定到新链接对应的文件描述符上,并初始化一个client绑定这个客户端链接;客户端发送请求命令,触发读就绪事件,服务端主线程不会经过socket去读取客户端的请求命令,而是先将client放入一个LIFO队列clientspendingread;在事件循环(EventLoop)中,主线程执行beforeSleephandleClientsWithPendingReadsUsingThreads,利用RoundRobin轮询负载均衡策略,把clientspendingread队列中的链接均匀地分配给IO线程各自的本地FIFO任务队列iothreadslist〔id〕和主线程本身,并且用iothreadspending〔id〕来记录每个线程的分配任务数量,因为线程需要读取这个iothreadspending〔id〕这个数量来消费任务,消费完成会初始化为0。IO线程经过socket读取客户端的请求命令(是通过iothreadsop这个变量来判断是读(IOTHREADSOPREAD)还是写(IOTHREADSOPWRITE),这里是iothreadsopIOTHREADSOPREAD),存入clientquerybuf并解析第一个命令,但不执行命令,主线程忙轮询,等待全部IO线程完成读取任务;主线程和全部IO线程都完成了读取任务(通过遍历iothreadspending〔id〕,把每个线程的分配任务数量累加起来如果和等于0代表多线程已经消费完了任务),主线程结束忙轮询,遍历clientspendingread队列,执行全部客户端链接的请求命令,先调用processCommandAndResetClient执行第一条已经解析好的命令,而后调用processInputBuffer解析并执行客户端链接的全部命令,在其中使用processInlineBuffer或者processMultibulkBuffer根据Redis协议解析命令,最后调用processCommand执行命令;根据请求命令的类型(SET,GET,DEL,EXEC等),分配相应的命令执行器去执行,最后调用addReply函数族的一系列函数将响应数据写入到对应client的写出缓冲区:clientbuf或者clientreply,clientbuf是首选的写出缓冲区,固定大小16KB,通常来讲能够缓冲足够多的响应数据,可是若是客户端在时间窗口内须要响应的数据很是大,那么则会自动切换到clientreply链表上去,使用链表理论上可以保存无限大的数据(受限于机器的物理内存),最后把client添加进一个LIFO队列clientspendingwrite;在事件循环(EventLoop)中,主线程执行beforeSleephandleClientsWithPendingWritesUsingThreads,利用RoundRobin轮询负载均衡策略,把clientspendingwrite队列中的链接均匀地分配给IO线程各自的本地FIFO任务队列iothreadslist〔id〕和主线程本身,并且用iothreadspending〔id〕来记录每个线程的分配任务数量,因为线程需要读取这个iothreadspending〔id〕这个数量来消费任务,消费完成会置为0。IO线程经过调用writeToClient(iothreadsopIOTHREADSOPWRITE)把client的写出缓冲区里的数据回写到客户端,主线程忙轮询,等待全部IO线程完成写出任务;主线程和全部IO线程都完成了写出任务(通过遍历iothreadspending〔id〕,把每个线程的分配任务数量累加起来如果和等于0代表多线程已经消费完了任务),主线程结束忙轮询,遍历clientspendingwrite队列,若是client的写出缓冲区还有数据遗留,则注册sendReplyToClient到该链接的写就绪事件,等待客户端可写时在事件循环中再继续回写残余的响应数据。
这里大部分逻辑和以前的单线程模型是一致的,变更的地方仅仅是把读取客户端请求命令和回写响应数据的逻辑异步化了,交给IO线程去完成,这里须要特别注意的一点是:IO线程仅仅是读取和解析客户端命令而不会真正去执行命令,客户端命令的执行最终仍是要回到主线程上完成。
相关视频推荐
手把手带你调试阅读redis源码
6种epoll的做法,从redis,memcached到nginx的网络模型实现
c后端绕不开的7个开源项目,每一个源码值得深入研究
需要CCLinux服务器架构师学习资料加qun812855908获取(资料包括CC,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCPIP,协程,DPDK,ffmpeg等),免费分享
2。2图解详细流程
3。IO多线程lockfree(无锁设计)
主线程和IO线程之间共享的变量有三个:iothreadspending计数器、iothreadsopIO标识符和iothreadslist线程本地任务队列。iothreadspending统计每个线程分配任务的数量,主线程在分配的时候,子线程是被换新的,它一直在执行100w次的CPU空转即自旋操作;当主线程把任务分配好之后,子线程会判断iothreadspending〔id〕不为0就去消费任务,消费完成会置为0。iothreadsopIOTHREADSOPWRITEsocket可写IOTHREADSOPREADsocket可读iothreadslist线程本地任务队列(链表)
lockfree无锁设计核心:
1。原子变量,不需要加锁保护iothreadspending变量在声明的时候加上了Atomic限定符:Atomicunsignedlong;Atomic是C11标准中引入的原子操作:被Atomic修饰的变量被认为是原子变量,对原子变量的操作是不可分割的(Atomicity),且操作结果对其他线程可见,执行的顺序也不能被重排。所以iothreadspending是属于线程安全的变量。
2。交错访问来规避共享数据竞争iothreadsop和iothreadslist这两个变量则是经过控制主线程和IO线程交错访问来规避共享数据竞争问题。IO线程启动以后会经过忙轮询和锁休眠等待主线程的信号,在这以前它不会去访问本身的本地任务队列iothreadslist〔id〕,而主线程会在分配完全部任务到各个IO线程的本地队列以后才去唤醒IO线程开始工做,而且主线程以后在IO线程运行期间只会访问本身的本地任务队列iothreadslist〔0〕而不会再去访问IO线程的本地队列,这也就保证了主线程永远会在IO线程以前访问iothreadslist而且以后再也不访问,保证了交错访问。iothreadsop同理,主线程会在唤醒IO线程以前先设置好iothreadsop的值,而且在IO线程运行期间不会再去访问这个变量,这也就变相保证了原子性。在源码srcserver。h中:externintiothreadsop;4。源码
源码真的太多了,拷贝到这里实在影响阅读,因此为了大家能迅速定位,我这里贴出地址哈。子线程入口Main函数处理read,write和解析操作IOThreadMain:源码文件3665行https:github。comredisredisblob64f6159646337b4a3b56a400522ad4d028d55dacsrcnetworking。c主线程执行回复入口函数handleClientsWithPendingWritesUsingThreads:3810行地址同上主线程执行读取数据入口函数handleClientsWithPendingReadsUsingThreads:3935行地址同上主线程初始化多线程入口函数initThreadedIO:3712行地址同上其他源码readQueryFromClient:2266行postponeClientRead:3908行地址同上