分布式服务下Quartz任务变为EREOR分析及解决

一、前言

在项目中遇到这样的一个问题:

服务spring-cloud-quartz-one中有一个Quartz任务:MyJob

服务spring-cloud-quartz-two中有两个Quartz任务:MyJob、MyJob2

当第一个服务开启MyJob任务,第二个服务开启MyJob2任务。结果是MyJob2任务总是不生效,但是MyJob是生效了的。在spring-cloud-quartz-two中,是没有任何报错信息的,但是在spring-cloud-quartz-one中有个报错:

2020-12-13 09:56:58.267 ERROR 10432 --- [SchedulerThread] o.s.s.q.LocalDataSourceJobStore          : Error retrieving job, setting trigger state to ERROR.

org.quartz.JobPersistenceException: Couldn't retrieve job because a required class was not found: com.xwj.quartz.job.MyJob2
    at org.quartz.impl.jdbcjobstore.JobStoreSupport.retrieveJob(JobStoreSupport.java:1393) ~[quartz-2.3.0.jar:?]
    at org.quartz.impl.jdbcjobstore.JobStoreSupport.acquireNextTrigger(JobStoreSupport.java:2864) [quartz-2.3.0.jar:?]
    at org.quartz.impl.jdbcjobstore.JobStoreSupport$41.execute(JobStoreSupport.java:2805) [quartz-2.3.0.jar:?]
    at org.quartz.impl.jdbcjobstore.JobStoreSupport$41.execute(JobStoreSupport.java:2803) [quartz-2.3.0.jar:?]
    at org.quartz.impl.jdbcjobstore.JobStoreSupport.executeInNonManagedTXLock(JobStoreSupport.java:3849) [quartz-2.3.0.jar:?]
    at org.quartz.impl.jdbcjobstore.JobStoreSupport.acquireNextTriggers(JobStoreSupport.java:2802) [quartz-2.3.0.jar:?]
    at org.quartz.core.QuartzSchedulerThread.run(QuartzSchedulerThread.java:287) [quartz-2.3.0.jar:?]
Caused by: java.lang.ClassNotFoundException: com.xwj.quartz.job.MyJob2
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381) ~[?:1.8.0_172]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[?:1.8.0_172]
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) ~[?:1.8.0_172]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[?:1.8.0_172]
    at java.lang.Class.forName0(Native Method) ~[?:1.8.0_172]
    at java.lang.Class.forName(Class.java:348) ~[?:1.8.0_172]
    at org.springframework.util.ClassUtils.forName(ClassUtils.java:275) ~[spring-core-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.scheduling.quartz.ResourceLoaderClassLoadHelper.loadClass(ResourceLoaderClassLoadHelper.java:81) ~[spring-context-support-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.scheduling.quartz.ResourceLoaderClassLoadHelper.loadClass(ResourceLoaderClassLoadHelper.java:86) ~[spring-context-support-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.quartz.impl.jdbcjobstore.StdJDBCDelegate.selectJobDetail(StdJDBCDelegate.java:852) ~[quartz-2.3.0.jar:?]
    at org.quartz.impl.jdbcjobstore.JobStoreSupport.retrieveJob(JobStoreSupport.java:1390) ~[quartz-2.3.0.jar:?]
    ... 6 more

 

二、问题分析:

在上面的错误日志中,有两点很重要的信息:

Error retrieving job, setting trigger state to ERROR.

意思就是说,找不到这个job,将触发器状态改为ERROR。查看 qrtz_triggers 表,发现MyJob2对应的任务状态确实变为ERROR了(也就是任务执行失败了)

分布式服务下Quartz任务变为EREOR分析及解决

查阅资料会发现Trigger的state有这样几种值:

WAITING:等待

PAUSED:暂停

ACQUIRED:正常执行

BLOCKED:阻塞

ERROR:错误

 

Couldn't retrieve job because a required class was not found: com.xwj.quartz.job.MyJob2

意思就是说,没有找到MyJob2对应的类。这就很奇怪了,spring-cloud-quartz-one服务中不应该会执行MyJob2任务啊。可以猜到,当两个服务共用一个Quartz库时,应该是需要有一种隔离机制的,将不同的服务的任务隔离开。那如何隔离呢?

分别将两个服务的日志级别调低一点:

logging:
  level:
    jdbc: off
    jdbc.sqltiming: error #记录sql执行的时间
    com.xwj: debug

重新启动下服务,然后分别开启MyJob和MyJob2任务。在spring-cloud-quartz-one服务中可以看到Quartz日志:

2020-12-13 10:47:37.867  INFO 9900 --- [_ClusterManager] j.sqltiming  : SELECT * FROM QRTZ_SCHEDULER_STATE WHERE SCHED_NAME = 'instance_one' 
 {executed in 1 msec}
2020-12-13 10:47:37.868  INFO 9900 --- [_ClusterManager] j.sqltiming  : UPDATE QRTZ_SCHEDULER_STATE SET LAST_CHECKIN_TIME = 1607827657867 WHERE SCHED_NAME = 'instance_one' 
AND INSTANCE_NAME = 'instance_id_one' 
 {executed in 0 msec}
2020-12-13 10:47:39.237  INFO 9900 --- [SchedulerThread] j.sqltiming  : SELECT TRIGGER_NAME, TRIGGER_GROUP, NEXT_FIRE_TIME, PRIORITY FROM QRTZ_TRIGGERS WHERE SCHED_NAME 
= 'instance_one' AND TRIGGER_STATE = 'WAITING' AND NEXT_FIRE_TIME <= 1607827664235 AND (MISFIRE_INSTR = -1 OR (MISFIRE_INSTR != -1 AND NEXT_FIRE_TIME >= 1607827599236)) ORDER BY NEXT_FIRE_TIME 
ASC, PRIORITY DESC 
 {executed in 1 msec}
2020-12-13 10:47:39.239  INFO 9900 --- [SchedulerThread] j.sqltiming  : SELECT * FROM QRTZ_TRIGGERS WHERE SCHED_NAME = 'instance_one' AND TRIGGER_NAME = '456' AND 
TRIGGER_GROUP = 'MyJob2' 
 {executed in 1 msec}
2020-12-13 10:47:39.241  INFO 9900 --- [SchedulerThread] j.sqltiming  : SELECT * FROM QRTZ_CRON_TRIGGERS WHERE SCHED_NAME = 'instance_one' AND TRIGGER_NAME = '456' 
AND TRIGGER_GROUP = 'MyJob2' 
 {executed in 1 msec}
2020-12-13 10:47:39.243  INFO 9900 --- [SchedulerThread] j.sqltiming  : SELECT * FROM QRTZ_JOB_DETAILS WHERE SCHED_NAME = 'instance_one' AND JOB_NAME = 'JOB_456' AND 
JOB_GROUP = 'MyJob2' 
 {executed in 1 msec}
2020-12-13 10:47:39.245 ERROR 9900 --- [SchedulerThread] o.s.s.q.LocalDataSourceJobStore : Error retrieving job, setting trigger state to ERROR.

通过上面的日志可以看到,可以看到Quartz任务的执行步骤为:

1、通过 SCHED_NAME 查询 QRTZ_SCHEDULER_STATE 表,并更新 LAST_CHECKIN_TIME 字段

2、通过 SCHED_NAME 查询 QRTZ_TRIGGERS 表中当前可以执行的所有任务(主要是返回TRIGGER_NAME, TRIGGER_GROUP)

3、通过上一步返回的信息,然后加上 SCHED_NAME 查询出具体的触发器详细信息

4、使用和第3步中一样的查询条件,查询 QRTZ_CRON_TRIGGERS 表,获取定时任务CRON_EXPRESSION

5、使用和第3步中一样的查询条件,查询 QRTZ_JOB_DETAILS 表,获取任务详细信息(包括JOB_CLASS_NAME)

当执行到第5步时,结果返回了MyJob2,然而spring-cloud-quartz-one服务中并没有类MyJob2,所以MyJob2任务一定会执行失败,将状态更新为ERROR了。

问题的原因终于找到了,但是如何解决呢?

 

三、解决问题:

其实不难发现,在Quartz所在服务启动时,会往 qrtz_scheduler_state 表插入一条数据,如下:

分布式服务下Quartz任务变为EREOR分析及解决

其中 SCHED_NAME 和 INSTANCE_NAME 分别是quartz.properties配置文件中的 instanceName 和 instanceId。并且Quartz在寻找当前可执行任务时,全部都会带上SCHED_NAME,所以只要将不同服务的SCHED_NAME设置为不一样,也就是将配置文件中 instanceName 设置为与其他服务不一样就行(instanceId最好也设置为不一样,设置为AUTO自动生成一个id也行)。

 

四、遇到的坑

如果是使用SpringBoot1.5 + org.quartz-scheduler,在配置文件中,设置 instanceName 是不生效的(也不知道为啥),默认是使用 schedulerFactoryBean 作为SCHED_NAME。如果想自己配置 SCHED_NAME,可以在创建SchedulerFactoryBean时,对 schedulerName 手动赋值,如下:

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory, DataSource dataSource) throws Exception {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        // 设置SCHED_NAME
        schedulerFactoryBean.setSchedulerName("instance_two");
        // 将spring管理job自定义工厂交由调度器维护
        schedulerFactoryBean.setJobFactory(jobFactory);
        // 设置覆盖已存在的任务
        schedulerFactoryBean.setOverwriteExistingJobs(true);
        // 项目启动完成后,等待2秒后开始执行调度器初始化
        schedulerFactoryBean.setStartupDelay(2);
        // 设置调度器自动运行
        schedulerFactoryBean.setAutoStartup(true);
        // 设置数据源,使用与项目统一数据源
        schedulerFactoryBean.setDataSource(dataSource);
        // 设置上下文spring bean name
        schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContext");
        // 设置配置文件位置
        schedulerFactoryBean.setConfigLocation(new ClassPathResource("/quartz.properties"));
        return schedulerFactoryBean;
    }

 

上一篇:windows服务+定时任务(quartz.net)+nancy


下一篇:定时任务实现四种方式