一、前言
在项目中遇到这样的一个问题:
服务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了(也就是任务执行失败了)
查阅资料会发现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 表插入一条数据,如下:
其中 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; }