使用XXL-JOB时,如何避免多台服务器重复调度任务?|工作踩坑系列

1.XXL-JOB介绍

  XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。支持通过Web页面对任务进行CRUD操作,操作简单,一分钟上手;支持动态修改任务状态、启动/停止任务,以及终止运行中任务,即时生效。
  官方地址中文版:www.xuxueli.com/xxl-job
  Gitee地址:gitee.com/xuxueli0323…

2.应用场景介绍

  我们将XXL-JOB调度系统部署在两台服务器实例上。使用XXL-JOB调度系统主要是执行各个业务系统的脚本,如发送短信、变更用户积分、生成BI报表等需求。

3.问题回顾

  job系统每天都正常的运行着,没发生过什么大问题。
  有天早上测试妹子向研发部门反馈问题:用户的每日积分、每周积分怎么变多了?刚好多了一倍,你们快看看吧,急!
  好家伙,这还了得,立马咨询坐在我旁边的大佬,看之前是否发生过这样的问题,大佬说很少发生,几乎没有。
  于是我找到处理每日积分的地方,发现有一个定时脚本在执行,每天晚上快凌晨时会执行一次,我随后登录job系统后台,查询该脚本,搜索该日期,发现了一个奇怪的问题,如图:

WX20210703-173403@2x.png

通过该截图发现了两个问题:
  1.该脚本任务在统一时间被调度了两次,为什么被调度了两次呢?因为我们有两台服务器实例,同时触发了调度任务。
  2.该脚本在间隔不到一分钟内重复执行了两次。

4.解决方案

  查看代码得知,这个脚本是每天只执行一次,但代码层面并没有判断是否已经执行过,多次调用就会多次计算。这就导致了计算数据时会出错,比如用户积分本应该减少10积分,由于调度系统调度了两次,这个用户的积分就减少了20积分。找到具体原因后就好解决了,其实就是避免脚本重复执行就可以了。

4.1使用XXL-JOB配置来避免重复调度(推荐)

  官网推荐的处理办法是利用“任务调度锁表“来避免集群同时调度的情况。

4.2在代码层面避免重复调度

方案1:

  在该脚本执行的地方判断下更新时间,判断今天是否已经执行过,这样不就可以搞定啦。
但这种处理会有另一个问题,还有一些脚本代码也没有判断是否已执行过。那需要修改的地方就很多了,需要向其他更便捷的办法。

方案2:

  先在配置文件中定义哪些脚本不能被重复调度、定义Redis过期时间。使用Redis记录该脚本已经执行过,第二次执行时判断是否已执行,如果已执行则退出执行流程。
实现流程:
1.先定义配置文件

<?php  
    return [
        //定义延迟调用的接口
        'job_delay_request_api_config' => [
           'cache_time' => 1800,//过期时间 秒
           'list' => [
                'UserChangeLog/day',//每日积分统计
                'UserChangeLog/week',//每周积分统计
           ],
        ],
    ];
?>
复制代码

2.在父类控制器中判断脚本是否已执行

<?php
    public function __construct()
    {
        //获取当前调度的控制器和方法名称,用来当作key值
        $apiUrl = CONTROLLER_NAME . '/' . ACTION_NAME;
        //获取配置项中定义延迟调用的接口
        $config = config('job_delay_request_api_config');
        //判断当前调用的接口不在延迟脚本配置数组中,可以执行,返回即可
        if (!in_array($apiUrl, $config['list'])) {
            return;
        }
        //判断是否已调用过该接口
        $key = 'job_delay_request_api:' . $apiUrl;
        $redis = new Redis();
        $result = $redis->get($key);
        //判断已调用,则不能再次调用,需等待redis过期,此处停止脚本运行
        if (!empty($result)) {
            exit('该调度任务已执行,执行时间:' . date('Y-m-d H:i:s', $result));
        }
        //标记job任务已执行,记录执行时间
        $redis->set($key, $config['cache_time'], time());
    }
?>
复制代码

5.总结

  目前采用的是4.2章节中的方案2,通过读取预先定义的配置,在代码层面判断做拦截,可以达到防止任务被重复调度的问题,并且可以指定哪些脚本需要被拦截。为什么没有采用4.1呢?由于我不是搞Java的哈哈,Java端的同事也对这个系统不太熟悉,所以没有采用4.1官网推荐的方式,后续会考虑4.1官网推荐的方式,会更加合理和稳定。感谢阅读,我们下次再见。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享