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系统后台,查询该脚本,搜索该日期,发现了一个奇怪的问题,如图:
通过该截图发现了两个问题:
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官网推荐的方式,会更加合理和稳定。感谢阅读,我们下次再见。