node多节点服务器重复处理定时任务的解决思路  
View on GitHub wznonstop

node多节点服务器重复处理定时任务的解决思路

2017-12-27-node多节点服务器重复处理定时任务的解决思路

最近有一个需求,需要使用每天用户留资的信息,导出到excel中,其实就是在每天的特定时间执行定时任务。本地开发完成之后,放到服务器上,发现,怎么回事,每次会生成两份数据? 看了下发现,是启了两个完全相同的服务,心说,这不是很简单,mongoose不是有unique属性吗,加一下。尴尬的是发现没生效,查了一圈,似乎删数据,重启数据库才能生效,应用场景明显不适合,只得另谋他法。想了下,新增数据的时候查一下,没有的话,就新增,已经存在了就不执行呗。 结果事实像冷冷的冰雨砸在了我脸上,这两个服务太同步了以至于,有时候查的时候还没有生成新的数据,导致最终结果有时候生成一条数据,有时候生成两条数据,一时间有点没辙,时间紧急,想了个很low的方案,遍历生成的excel所在的文件夹,获取到文件名,返回到前端,进行展示。

但是这个方案存在的问题实在太多了,首先,生成的excel文件存储在服务器上,会占用服务器内存,在更新代码的时候,还要做备份,不然就丢了。针对这个问题,采取的解决思路是,在定时任务中,只是执行生成查询条件的数据,存入数据库中, 在前端页面点击相应的链接时,使用存入数据库的查询信息进行查询,同步获取数据,直接返回给浏览器,这样既不占内存,又能实现不阻塞的下载,体验更好,还省资源,同时避免了很多不必要的问题。

这样,矛盾就又回到了刚开始那个问题上,怎么保证定时任务只执行一次呢?经过一番思考和查找后,终于找到了合适的解决方案。

首先要明确的是,想要保证定时任务只执行一次,那么,一定需要找到一个标志的东西,保证不论有多少个服务器,它都一定是唯一的,那么,就可以创建一个model,假设叫做tasklock,model里面的一个字段记录服务器的标识,假设这个字段叫pid,到了要执行定时任务的时间,所有的服务器都去执行修改pid的操作,那么一段时间,比如1s之后,pid一定停留在记录某个服务器标识的状态。 这时候,所有的服务器再去查model里的第一条数据的pid,只有pid与model里的pid相同的服务器才去执行具体的定时任务,其他的服务器啥也不用干。



//利用node-schedule模块在每天0点执行定时任务
schedule.scheduleJob('0 0 0 * * *', function(){
      const timing_pid = getLocalIps() + '_' + process.pid;   //每个服务的唯一标识
      const timing_time = Date.now();

      const opt = {
        timing_status: 1,
        timing_pid,
        timing_time
      }
      //所有服务器同时更新tasklock中的第一条数据
      TasklockService.updateTasklock(opt).then( (doc) => {
        setTimeout(function () {
          //获取tasklock的数据
          TasklockService.getTasklock().then((data) => {
            if (data[0].task_pid === task_pid) {
              //logger.info('开始执行定时任务')
              //执行定时任务的代码,完成时,执行任务的服务器将tasklock中第一条数据的task_status改回0
            } else {
              // logger.info(`被人抢了啊${data[0].task_pid}----自己是${task_pid}`)
            }
          })
        }, 1000)
      });
    });

那么,如果执行任务的那台服务器在执行任务的过程中挂了怎么办呢,这就需要引入另外两个字段:task_status, task_time,分别记录任务的执行状态和开始执行任务的时间。到了要执行定时任务的时间,所有服务器都去更改tasklock这个model中的第一条数据。与此同时,预估一下任务执行所需要的时间,假设为5分钟,那么,在node-schedule的回调里再设置一个定时器,5分钟之后去tasklock中查询第一条数据的task_status。如下:



setTimeout(function () {
    //获取tasklock的数据
    TasklockService.getTasklock().then((data) => {
      if (data[0].task_status === 1) {    //等于1说明任务执行超时
        //所有服务器重新进行争夺
        //记录的task_time能确定正常情况下任务开始执行的时间,保证依据时间进行数据查询时,能依据该时间点准确确定时间范围,避免查询数据时有遗漏
      } else {
        // logger.info(`被人抢了啊${data[0].task_pid}----自己是${task_pid}`)
      }
    })
  }, 300000)

完成任务后,由执行任务的服务器去更改tasklock中第一条数据,将task_status重新至为0。如果5分钟后,查询到的task_status依然为1,那么就说明任务超时,执行任务的那台服务器可能遇到了什么奇怪的问题。这时候,就让所有服务器重新进行争夺,即去更改tasklock这个model中的第一条数据,将pid改为自己的特定标识,再在1s之后依据pid确定执行任务的服务器,然后执行具体任务。 整个过程如下图: timingtask