当前位置: 首页 > 科技观察

10w个定时任务,如何高效触发超时

时间:2023-03-13 14:20:10 科技观察

1.由来很多时候,业务需要定时任务或者定时超时。当任务负载较大时,可能需要维护大量定时器或执行低效扫描。例如:58到家APP实时消息通道系统维护一个从APP到服务器的TCP连接,供每个用户实时收发消息。对于这个TCP连接,有这样一个需求:“如果连续30秒没有请求包(比如login,message,keepalive包),服务器会把用户的状态设置为离线”。其中单机TCP同时在线量约10w,keepalive请求包约30s,吞吐量约3000qps。一般来说,如何实现这种需求呢?1、《轮询扫描法》1)使用一个Map2)当用户uid有请求包时,实时更新Map3)启动一个定时器,当Map不为空时2、《多定时器触发法》1)使用一个Map2)当一个用户uid有一个请求包时,实时更新这个Map,同时为这个uid请求包启动一个定时器,30s后触发解决方案1:只启动一个定时器,但是轮询是必需的,效率较低。方案二:不需要轮询,但是每个请求包都需要启动一个定时器,比较耗资源,尤其是同时在线量大的时候,很容易CPU占100%,如何高效维护和触发大量定时/超时任务是本文要讨论的问题。2.环形队列方法废话不多说,三个重要的数据结构:1)30s超时,创建一个索引从0到30的环形队列(本质上是一个数组)2)环形上的每个槽都是一个Set,任务Set3)还有同时也是一个Map:1)启动一个定时器,每隔1s在上面的循环队列中移动一格,0->1->2->3...->29->30->0。..2)有一个CurrentIndex指针来标识刚刚检测到的slot1。当一个用户uid的请求报文到达时:1)从Map结构中,找出uid存放在哪个slot2)从这个slot的Set结构中3)再次把uid添加到新的slot中,是哪个slot=>CurrentIndex指针指向的前一个slot,因为这个slot会在30s后被定时器扫描(4)updateMap,这个uid对应slot2的index值,哪些元素会超时?CurrentIndex每秒移动一个slot,这个slot对应的Set中的所有uid都应该集体超时!如果请求数据包在最后30秒内到达,则必须放在CurrentIndex的前一个槽中。CurrentIndex所在的slot对应Set中所有在最近30s内没有收到请求包的元素。因此,在没有超时的情况下,CurrentIndex扫描的每个slot的Set中应该没有元素。3.优点:(1)只需要一个定时器(2)定时器只需要每1s触发一次,对CPU的消耗非常小,总结一下循环队列方式是一种通用的方式,任何任务都可以在Set和Map,而本文中的uid就是最简单的例子。HashedWheelTimer也是类似的原理。有兴趣的同学可以百度一下这个数据结构,Netty中的一个工具类,希望大家有所收获。【本文为专栏作者《58神剑》原创稿件,转载请联系原作者】点此阅读更多该作者好文