分散式服務下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_NAME 和 INSTANCE_NAME 分別是quartz.properties配置檔案中的 instanceName 和 instanceId。並且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; }