1. 程式人生 > 實用技巧 >分散式服務下Quartz任務變為EREOR分析及解決

分散式服務下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了(也就是任務執行失敗了)

查閱資料會發現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_NAMEINSTANCE_NAME 分別是quartz.properties配置檔案中的 instanceNameinstanceId。並且Quartz在尋找當前可執行任務時,全部都會帶上SCHED_NAME,所以只要將不同服務的SCHED_NAME設定為不一樣,也就是將配置檔案中instanceName設定為與其他服務不一樣就行(instanceId最好也設定為不一樣,設定為AUTO自動生成一個id也行)。

修改配置如下:

org.quartz.scheduler.instanceName = instance_two
org.quartz.scheduler.instanceId = instance_id_two

再次檢視 qrtz_scheduler_state 表,會發現表中多了一條資料:

此時,不同的服務開啟任務後,就會通過 SCHED_NAME 隔離開了。

四、遇到的坑

如果是使用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;
    }