定时任务的常见触发方式

中间件项目中,经常会有下面的场景:

  • client的定时重试
  • client定时向server端发心跳包
  • server端对client的判活

这种其实都是在某一个时间点触发一些任务,但是当任务量很大时,怎么做比较高效呢?

比如client定时向server发心跳包,在server端如何对client进行判活呢?一般我们的做法主要有下面的几种,当任务量很大的时候我们一般都会采样环形队列/HashedWheelTimer方法。

轮询扫描

轮询扫描是最简单的处理方式,也非常的常见:

  • 用一个Map来记录每一个client最近一次请求时间last_packet_time
  • 当client有请求包来到,实时更新这个Map
  • 同时有一个线程来专门的不断扫描这个map,比如当检查client的last_packet_time是否超过30s,如果超过则进行超时处理

多timer触发

  • 用一个Map来记录每一个client最近一次请求时间last_packet_time
  • 当某个client有请求包来到,实时更新这个Map,并同时对这个client的请求包启动一个timer,30s之后触发
  • 每个client的请求包对应的timer触发后,看Map中,查看这个client的last_packet_time是否超过30s,如果超过则进行超时处理

环形队列/HashedWheelTimer

这种方案简单描述如下:

  • 环形队列,本质是一个数组,比如30s超时,就创建一个index从0到30的环形队列
  • 环上每一个slot是一个Set,表示:任务集合
  • 同时还有一个Map,记录某个client落在环上的哪个slot里
  • 同时启动一个timer,每隔1s,在上述环形队列中移动一格,移动到数组最后一个元素时候,又从第一个开始,也就是:0->1->2->3…->29->30->0…
  • 有一个Current Index指针来标识刚检测过的slot

当某client有请求包到达时:

  • 从Map结构中,查找出这个client存储在哪一个slot里
  • 从这个slot的Set结构中,删除这个client
  • 将client重新加入到新的slot中,具体是哪一个slot呢 => Current Index指针所指向的上一个slot,因为这个slot,会被timer在30s之后扫描到
  • 更新Map,这个client对应slot的index值

哪些元素会被超时掉呢?

因为Current Index每秒种移动一个slot,这个slot对应的Set中所有client都应该被集体超时!如果最近30s有请求包来到,一定被放到Current Index的前一个slot了,Current Index所在的slot对应Set中所有元素,都是最近30s没有请求包来到的。所以,当没有超时时,Current Index扫到的每一个slot的Set中应该都没有元素。

优势:

  • 只需要1个timer
  • timer每1s只需要一次触发,消耗CPU很低
  • 批量超时,Current Index扫到的slot,Set中所有元素都应该被超时掉
comments powered by Disqus