集群下的quartz出现重复调度的问题解决

我们采用 xxl-job 来作为我们的调度任务中心,而 xxl-job 采用的是 quartz 作为它的调度内核。

我们在部署的时候,采用了2台服务器集群的方式,有一天在分析一个工单的时候发现这2台服务器同时执行了某个时间段的调度2次,于是开始了这个问题查找分析过程。

首先我们找到了这篇文章:多个xxl-job-admin节点,任务重复执行问题,这篇文章描述了为什么会发生这个问题,以及该如何解决,但是这篇文章写得比较简短,还是不明白。

接着我们找到了这篇文章:记一次Quartz重复调度(任务重复执行)的问题排查,这篇文章的作者花了很多心思写这篇文章,写得很透彻。我们通过一边参照他的文章,一边看源代码,一边再进行不同阶段跟踪测试,证实了这篇文章所写得完全正确,也证实了他的解决办法是正确的。

下面就这个问题的出现原因再啰嗦几句,希望能更进一步的解惑:
下面这张图非常重要,我这边摘抄过来(链接地址没法用,所以我摘抄过来,希望作者理解一下这里用来说明问题):

  1. 上面图中出现的那几个Lock,都是基于数据库的分布式锁,这个分布式锁采用了数据库的for update行级锁的机制来实现。这个机制保证了在一开始的时候只有1个调度服务才能获取到调度任务。

  2. 上图中的第一个Lock,非常关键,这个Lock是出现重复调度的关键所在,但是 quartz 默认这个 Lock 是空的!!!这就会出现在上面文中所说的问题,2个调度服务同时发出请求,第1个非常快速的完成,会导致第2个也能在瞬间拿到。具体可以参见这个过程:

  3. 上图中的作者,有部分语言描述有丁点漏洞,上图中的“线程A”,“线程B”,会让读者认为只是在一个进程中会出现重复调度的问题,其实不然,应该改成“进程A”,”进程B”

springboot下报错“MethodInvokingJobDetailFactoryBean is not serializable”

quartz是在java语言中用得最为广泛的任务调度框架,spring对quartz进行了集成,方便了开发和使用。

在springboot下,我们对一个采用quartz的app进行多个集群部署的时候,对于quartz的集群采用数据库集群方式,报了“MethodInvokingJobDetailFactoryBean is not serializable”的错误。

原因是spring为了让开发更方便,耦合性更低,对quartz中 JobDetailFactoryBean 用 MethodInvokingJobDetailFactoryBean 进行了封装,假如quartz是单机运行那是没有关系的,但是当采用数据库方式进行集群的时候,quartz会把 JobDetailFactoryBean 序列化存储到数据库中,假如用 MethodInvokingJobDetailFactoryBean 的话,就会报错,说不能序列化。

解决的办法参考这篇文章:使用Spring + quartz集群持久化时注意事项,但是这篇文章中只讲了在spring下如何修改。

下面讲一下在 springboot 中如何修改:
一、需要将原先定义的pojo 的 Job 改成实现 Job 接口:

@Component("myJobHandler")
public class MyJobHandler implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("do it ......");
    }

}

二、在配置中如下设置:

@Bean(name = "myJobDetailFactoryBean")
    public JobDetailFactoryBean getMyJobDetailFactoryBean() {
        JobDetailFactoryBean jobDetail = new JobDetailFactoryBean();
        jobDetail.setJobClass(com.champbay.quartztest.MyJobHandler.class);
        jobDetail.setDurability(true);
        return jobDetail;
    }

    @Bean(name = "myCronTriggerFactoryBean")
    public CronTriggerFactoryBean getMyCronTriggerFactoryBean(
            @Qualifier("myJobDetailFactoryBean") JobDetailFactoryBean jobDetail) {
        CronTriggerFactoryBean tigger = new CronTriggerFactoryBean();
        tigger.setJobDetail(jobDetail.getObject());
        // 每1分钟     
        tigger.setCronExpression("0 0/1 * * * ?");
        return tigger;
    }

本文结束。