封装定时任务框架的正确方式

代码的执行路径和入口

代码并不是从开始一直运行到结束,经历顺序、分支、循环的控制结构,经历函数、类和对象等各种封装就可以了,工具脚本是这样的,只执行一次,传入参数或者修改配置,然后就会执行或计算出你想要的结果,客户端和服务端的代码都是长期运行的,有阻塞、并发等概念,涉及到多线程甚至多进程。服务端的代码逻辑可能是在接收到请求之后再执行,或者某个时间自动执行,客户端也是一样,有的代码是用户操作触发了什么事件才会执行,而有的是定时的自动执行的。

定时任务的周期与复杂度

定时执行的任务根据有没有周期概念和周期的长短复杂度也不同,有的任务是周期性的以天为单位的定时执行,比如每天的数据和日志备份,有的任务只执行一次或者只是很短的周期,比如我们云课堂中纪律树的成长,下课的提醒。很多后端应用的框架都会提供定时任务的功能,比如eggjs的schedule,比如java生态的quartz。这些是周期比较长的定时任务,因为服务器是不会停止的。客户端用到的定时任务一般周期都较短,框架或者运行平台提供的也都比较简单,比如浏览器自带的setInterval、setTimeout,egret的Timer等。

我们封装的定时任务框架TimeReminder

我们产品所面向的场景是教室中的一次上课过程,其中有一些需要定时任务的地方,比如下课的提醒,比如纪律树的自动生长。这些任务可以单独去定时,但是这样太过散乱,不易于管理和维护,所以我们封装了一个定时任务队列的库,叫做TimeRemider,功能是添加一个时间和该时间要执行的任务,到时间后会自动执行,这和直接用setInterval或者setTimout的区别有两个:

  1. 把所有定时任务集中到了一起来管理
  2. 定时器部分通过子进程的方式来提升性能

这部分功能比较独立,因此我们把他抽取成了一个单独的项目,以一个node module的形式来被我们业务的项目所使用,通过版本号的更新来迭代升级。

TimeReminder中存在的问题

最近我在读这部分的代码,对定时功能实现的方式、代码职责和结构的划分、暴露出去的api还有配置方面都有一些自己的看法。

定时功能的实现方式

time-reminder的定时器是用setInterval来实现的,通过定时轮询,每次轮询取出任务队列中最近的一个任务,判断是否需要执行,如果需要执行,则通知主进程执行这个任务,如果任务过期则删掉该任务。

因为轮询是有一定间隔的,所以这里需要判断当前时间是否在这个轮询周期的时间段内。另外,这里的offsetTime是和服务器时间的差值,项目中请求都会在接收到响应之后根据服务器的时间来更新这个offset,我觉得这里只要在其中一个接口校准一次就好了,因为服务器的时间也不会手动调整。

这里的实现方式是基于setInterval,所以才会有定时的轮询和时间段的计算。其实这里用setTimeout也可以,唯一有问题的是offset更新的问题,setInterval在下一个轮询的计算时就能感知到更新,而se'tTimeout感知不到,需要在offset更新后,手动的去clear掉所有的timer,然后基于新offfset算出的时间重新setTimeout。两种方式各有优缺点。

代码的职责和结构的划分

定时任务涉及到定时器和任务队列两个方面,


现在的版本中定时器是基于setInterval来实现的,考虑到性能,把他放到了子进程中去(我们是基于electron做的客户端,可以使用node),而主进程负责任务队列的维护和任务的执行。现在的目录划分很简单:

index.js是主进程的代码,主要是任务队列的维护和定时器的启动、停止等。core/adjust-timer.js是子进程的代码,里面是定时器的轮询和在检测到有任务要执行时通知主进程执行的逻辑。

代码职责方面我觉得是有问题的,如上面的架构图,我觉得定时器应该是纯粹的,只有基于setInterval的根据设定的时间不断轮询,或者基于setTimeout的定时通知的逻辑,而不应该包含判断任务队列是否有要执行任务的逻辑。而现在这部分逻辑是直接写在定时器里的。这样不但使得不能透明的从setInterval替换成setTimeout,也使得将来如果要适应更多平台(不支持node的进程)的成本增加。

合理的架构应该是多层次多模块的,层与层之间单项依赖,模块与模块之间职责明确,基于抽象的约定来通信,这样才能做到可以灵活的替换实现方案,比如把setInterval替换成setTimeout,比如把进程的方式去掉。


如图,至少应该有2个层次,定时器和任务队列是底层实现功能的部分,主进程和子进程的代码是node环境下的适配方式,然后再提供一个index.js暴露全局api。

这样的架构和对应的目录结构是易于扩展和替换实现方案的,vue在3.0中把observer独立成顶层文件夹,就是为了替换成proxy更方便,这里也是一样。

暴露出去的api

实现功能之后要暴露出一些api去,供外部使用,暴露出去的api对应着定时器和任务队列,也有两部分,一部分是添加、删除、清空定时任务的,一部分是启动、停止定时器以及修改计时offset的。

现在暴露出去的api如下:

addTimeListener
removeTimeListener
hasOwnId

clear
start
stop
getCurrentServiceTime
updateOffset

8个api前3个是定时任务的,后5个是定时器相关的,但是从名字上不能明确的区分出各自的功能,我觉得如下的命名会更好一些;

addTimedTask
removeTimedTask
clearTimedTask

startTimer
stopTimer
setTimeOffset

getCurrentServiceTime是获取当前服务器时间的,虽然在请求响应的时候设置到了这里,但是这并不是定时任务的功能,不应该放到这里面,可以在响应的时候再保存一份到别的地方。

配置

定时任务中有很多可以配置的地方,比如扩展成多平台之后的平台选择,比如定时器setInterval和setTimeout两种实现的选择,比如是否打印日志等。

可以像eslint、babel等提供一个配置文件放在项目下,支持json等配置方式,可以叫timerTaskQueue.config.js。

module.exports = {
   log: false,//是否打印日志
   platform: 'node',//使用定时任务的平台
   timer: 'interval'//定时器的实现方式
}

甚至可以提供插件扩展的机制或者一系列内置的功能供用户自己去选择。

其他的问题

代码中还有很多命名和实现的具体问题:

比如分了handlerList和taskList两部分,本意是handler可以复用,但是却没有提供复用handler的合理机制,像提供handler的name注册机制等。

比如taskList中的task如果一个time有多个任务,会组织成如下的结构,我觉得这个也是没有必要的,扁平化的放多份就可以,这样组织还有维护成本。

{
   time: 2323232
    ids: [id1,id2]
}

总结

请求、定时任务、事件都是代码触发的方式,或者说执行的入口,定时任务根据周期的长短复杂度也不同,后端或者客户端的框架都提供了定时任务的功能(eggjs、quartz、egret、web等)。

我们的项目为了集中管理定时任务,封装了一个定时任务框架叫time-reminder,提供定时器和任务队列两方面的功能。因为是node平台,考虑到性能使用了子进程的方式,并且定时器的实现是setInterval。我提出了一些重构的思路,包括代码架构和目录结构的调整、支持配置、改进暴露出去的api,以及一些代码的细节问题。

真正做一个通用的东西,和做只能适应一种业务场景的东西是完全不一样的,我们既然把他抽取了出来,就要使得它更加的通用,完善的差不多之后可以考虑开源,到时候一定要支持多平台、支持配置、暴露的顶层api更加优化,甚至提供插件功能。同时书写文档、demo和测试用例。会继续完善下去。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容