图片来自抱兔网但是单线程的设计也给Redis带来了一些问题:只能使用一个CPU核。如果删除的key过大(比如Set类型有百万级对象),会导致服务端阻塞数秒,QPS难以提升。为了解决以上问题,Redis在4.0和6.0版本分别引入了LazyFree和多线程IO,并逐步过渡到多线程。下面将详细介绍。单线程原理说Redis是单线程的,那么单线程是怎么体现的呢?它如何支持客户端的并发请求?为了搞清楚这些问题,我们先来了解一下Redis是如何工作的。Redis服务端是一个事件驱动程序,服务端需要处理以下两类事件:文件事件:Redis服务端通过sockets与客户端(或其他Redis服务端)连接,文件事件是服务端对套接字操作。服务端与客户端的通信会产生相应的文件事件,服务端会通过监听和处理这些事件来完成一系列的网络通信操作,如连接接受、读取、写入、关闭等。时间事件:一些操作Redis服务器中的(如serverCron函数)需要在给定的时间点执行,时间事件是服务器对此类定时操作的抽象,如过期键清理、服务状态统计等。事件调度展示在上图中。Redis抽象了文件事件和时间事件,时间轮换训练器会监控I/O事件表。一旦文件事件就绪,Redis会先处理文件事件,再处理时间事件。在以上所有的事件处理中,Redis都是以单线程的形式处理的,所以Redis是单线程的。另外,如下图所示,Redis基于Reactor模型开发了自己的I/O事件处理器,即文件事件处理器。多路复用器Redis在I/O事件处理中使用了I/O??多路复用技术,同时监听多个socket,并为socket关联不同的事件处理函数,通过线程实现。多客户端并发处理。由于这样的设计,在数据处理的时候避免了加锁操作,既使得实现足够简单,又保证了它的高性能。当然Redis单线程只是指它的事件处理。事实上,Redis并不是单线程的。比如生成一个RDB文件,它会fork一个子进程来实现。当然,这不是本文要讨论的内容。LazyFree机制上面我们知道,Redis在处理客户端命令时是单线程运行的,处理速度非常快,在此期间不会响应其他客户端请求。但是如果客户端向Redis发送耗时命令,比如删除一个包含百万对象的Setkey,或者执行flushdb或者flushall操作,Redis服务端需要回收大量的内存空间,导致服务端被卡了几秒。对于负载很重的缓存系统来说,这将是一场灾难。为了解决这个问题,在Redis4.0版本中引入了LazyFree,将慢速操作异步化,这也是在事件处理上向多线程迈进了一步。作者在他的博客中说,解决操作慢,可以采用渐进式处理,即添加一个时间事件,比如删除一个有百万对象的Setkey,一次只删除大key的一部分数据,最终实现大key的删除。但是这种方案可能会导致采集速度赶不上创建速度,最终导致内存耗尽。所以Redis最终的实现是异步删除大key,使用非阻塞删除(对应命令UNLINK)。largekey的空间回收是通过单独的线程实现的。主线程只做关系释放,可以快速返回继续处理。其他事件,避免服务器长时间阻塞。以删除(DEL命令)为例,看看Redis是如何实现的。下面是删除函数的入口:voiddelCommand(client*c){delGenericCommand(c,server.lazyfree_lazy_user_del);}/*ThiscommandimplementsDELandLAZYDEL.*/voiddelGenericCommand(client*c,intlazy){intnumdel=0,j;for(j=1;jargc;j++){expireIfNeeded(c->db,c->argv[j]);//根据配置判断DEL执行时是否以惰性形式执行intdeleted=lazy?dbAsyncDelete(c->db,c->argv[j]):dbSyncDelete(c->db,c->argv[j]);如果(删除){signalModifiedKey(c,c->db,c->argv[j]);notifyKeyspaceEvent(NOTIFY_GENERIC,"del",c->argv[j],c->db->id);server.dirty++;numdel++;}}addReplyLongLong(c,numdel);}其中lazyfree_lazy_user_del为是否修改DEL命令的默认行为。一旦启用,DEL将以UNLINK的形式执行。同步删除很简单,只要删除key和value,如果有内部引用,则进行递归删除,这里不再介绍。让我们看看异步删除。Redis在回收对象时,会先计算回收收益。只有当回收收益超过一定值时,才会打包成一个Job加入到异步处理队列中。否则直接同步回收,效率更高。循环收益的计算也很简单。比如对于String类型,循环收益值为1,对于Set类型,循环收益为集合中的元素个数。/*Deleteakey,value,andassociatedexpirationentryifany,fromtheDB.*Ifthereareenoughallocationstofreethevalueobjectmaybeputinto*alazyfreelistinsteadofbeingfreedsynchronously.Thelazyfreelist*willbereclaimedinadifferentbio.cthread.*/#defineLAZYFREE_THRESHOLD64intdbAsyncDelete(redisDb*db,robj*key){/*Deletinganentryfromtheexpiresdictwillnotfreethesdsof*thekey,becauseitissharedwiththemaindictionary.*/if(dictSize(db->expires)>0)dictDelete(db->e??xpires,key->ptr);/*如果这个值是由几个分配组成的,tofreeinalazyway*实际上只是更慢...Sounderacertainlimitwejustfree*theobjectsynchronously.*/dictEntry*de=dictUnlink(db->dict,key->ptr);if(de){robj*val=dictGetVal(de);//计算值的回收收益size_tfree_effort=lazyfreeGetFreeEffort(val);/*如果释放对象太多,通过将对象添加到惰性空闲列表来在后台执行*。*请注意,如果对象是共享的,现在收回它不是*可能的。红色的一部分iscore可能会调用crRefCount()来保护*对象,然后调用dbDelete()。这种情况下我们会fall*throughandreachthedictFreeUnlinkedEntry()调用,那将*等同于只调用decrRefCount()。*///只有回收收益超过一定值才会进行异步删除,否则会退化为同步删除if(free_effort>LAZYFREE_THRESHOLD&&val->refcount==1){atomicIncr(lazyfree_objects,1);bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);dictSetVal(db->dict,de,NULL);}}/*Releasethekey-valpair,或者只是thekeyifwesettheval*fieldtoNULLinordertolazyfreeitlater.*/if(de){dictFreeUnlinkedEntry(db->dict,de);if(server.cluster_enabled)slotToKeyDel(key->ptr);return1;}else{return0;}}通过引入一个threadedlazyfree,Redis实现了SlowOperation的惰性操作,避免了删除大键、FLUSHALL、FLUSHDB时服务器阻塞。当然,在实现这个功能的时候,不仅引入了lazyfreethreads,还对Redis聚合类型的存储结构进行了改进。因为Redis内部使用了很多共享对象,比如客户端输出缓存。当然,Redis并没有使用锁来避免线程冲突。锁竞争会导致性能下降。相反,它删除共享对象并直接使用数据复制。如下,3.x和6.x中ZSet节点值的不同实现://3.2.5ZSet节点实现,值定义robj*obj/*ZSETsuseaspecializedversionofSkiplists*/typedefstructzskiplistNode{robj*obj;doublescore;structzskiplistNode*backward;structzskiplistLevel{structzskiplistNode*forward;unsignedintspan;}level[];}zskiplistNode;//版本6.0.10ZSet节点实现,值定义为sdsele/*ZSETsuseaspecializedversionofSkiplists*/typedefstructzskiplistNode{sdsele;doublescore;structzskiplistLeforzNodebackward*skveltzskiplongspan;unsignedleveled[];}zskiplistNode;去掉共享对象不仅实现了lazyfree功能,也让Redis的多线程成为可能,正如作者所说:Nowthatvaluesofaggregateddatatypesarefullyunshared,andclientoutputbuffersdon'tcontain共享对象,还有很多可以利用的地方。例如,终于可以在Redis中实现线程化I/O,从而使不同的客户端由不同的线程服务。这意味着我们只有在访问数据库时才会有一个全局锁,但是客户端读/写系统调用,甚至客户端发送的命令的解析,都可能发生在不同的线程中。多线程I/O及其局限Redis在4.0版本引入了LazyFree,从此Redis有一个LazyFree线程专用于大key的回收,聚合类型的共享对象是也去掉了,这带来了多线程的可能。Redis不负众望,在6.0版本实现了多线程I/O。①实现原理正如之前官方的回复,Redis的性能瓶颈不在CPU,而在内存和网络。所以6.0发布的多线程并没有把事件处理改成多线程,而是在I/O上。另外,如果把事件处理改成多线程,不仅会导致锁竞争,还会导致频繁的上下文切换。即使使用分段锁来减少竞争,Redis内核也会有较大的变化,性能不一定会有明显的提升。多线程IO实现如上图红色部分所示,是Redis实现的多线程部分,利用多核来分担I/O读写负载。事件处理线程每拿到一个可读事件,就会把所有就绪的可读事件分配给I/O线程等待。所有I/O线程完成读操作后,事件处理线程开始进行任务处理。处理完成后,写事件也分配给I/O线程,等待所有I/O线程完成写操作。以读取事件处理为例,我们看一下事件处理线程任务分发过程:0;//将等待的客户端分配给I/O线程while((ln=listNext(&li))){client*c=listNodeValue(ln);inttarget_id=item_id%server.io_threads_num;listAddNodeTail(io_threads_list[target_id],c);item_id++;}.../*Waitforalltheotherthreadstoendtheirwork.*///等待所有I/O线程完成处理while(1){unsignedlongpending=0;for(intj=1;jconn);}else{serverPanic("io_threads_opvalueisunknown");}}listEmpty(io_threads_list[id]);io_threads_pending[id]=0;if(tio_debug)printf("[%ld]Done\n",id);}}②Limitations从上面的实现来看,6.0版本的多线程并不完整Multi-threading,I/O线程只能同时进行读或写操作。在此期间,事件处理线程一直处于等待状态,不是流水线模型,存在大量的循环等待开销。Tair的多线程实现更加优雅。如下图,Tair的MainThread负责客户端连接建立等,IOThread负责请求读取、响应发送、命令解析等,WorkerThread专门负责事件处理。IOThread读取用户的请求并进行解析,然后将解析结果以命令的形式放入队列中,发送给WorkerThread进行处理。WorkerThread在command处理完成后产生response,通过另一个queue发送给IOThread。为了提高线程的并行性,在IOThread和WorkerThread之间使用无锁队列和管道进行数据交换,整体性能会更好。总结Redis4.0引入了LazyFree线程,解决了大键删除导致服务器阻塞的问题,并在6.0版本引入了I/OThread线程,正式实现了多线程。但是和Tair相比,就不是很优雅了,性能提升也不多。根据压力测试,多线程版本的性能是单线程版本的2倍,Tair多线程版本的性能是单线程版本的3倍。在笔者看来,Redis的多线程无非就是两种思路,I/O线程化和Slowcommandsthreading。正如作者在其博客中所说:I/O线程不会在RedisAFAIK中发生,因为经过深思熟虑后,我认为没有充分的理由就很复杂。许多Redis设置实际上是网络或内存绑定的。此外,我真的相信无共享设置,所以我想扩展Redis的方式是改进对在同一主机上执行的多个Redis实例的支持,尤其是通过Redis集群。相反,我真正想要的是慢操作线程,以及Redis模块系统,我们已经朝着正确的方向前进。然而在未来(不确定是在Redis6还是7中)我们将在模块系统中获得键级锁定,这样线程就可以完全控制键来处理慢速操作。现在模块可以执行命令并可以以完全分离的方式为客户端创建回复,但仍然需要全局锁来访问共享数据集:这将消失。Redis作者更倾向于使用集群来解决I/O线程,尤其是在6.0版本发布的原生RedisClusterProxy的背景下,让集群更易用。此外,作者更喜欢慢操作线程(如4.0版本中发布的LazyFree)来解决多线程问题。后续版本会不会完善IOThread的实现,使用Module优化慢操作,确实值得期待。作者:静同学编辑:陶家龙来源:juejin.cn/post/6928407842009546766