Quartz分布式集成之殇:破解JobStoreTX消失与数据源Null

174 阅读5分钟

背景

公司现有项目为基于Quartz实现的定时任务的项目由于架构的要求需要对当前架构进行调整 简单介绍当下的情况就是项目中有两个服务模块:manage和task。

最初的规划是manage负责基于Quartz框架最基础的API对前端提供任务管理的接口,当前模块不开启任务的调度;

task模块中只进行任务的执行和调度,不参与其他业务的实现。而具体的任务全部抽出到一个新的bean模块中(当前bean不为服务),然后manage和task都会依赖于这个bean。

  • 线上部署方案:
    • Manage服务:单节点部署(无状态)
    • Task服务:可弹性伸缩的集群部署

当前架构痛点

按照当前架构开发并且运行一段时间后发现问题如下:

  • manage服务其实只对外提供了Quartz管理任务的基础API,并没有进行任务的调度但是还需要引入很多quartz相关依赖以及bean模块,这样就导致项目结构很混乱
  • 由于bean处于一个被两个服务依赖的情况,即使已经将bean模块中所需的配置已经单独抽出一个配置文件但还是需要分别在manage和task中重复引入。
  • 开发完任务的具体实现在测试阶段很多时候只走了manage模块的场景,遗漏task中是否可以正常

架构优化

综上所述做出如下修改

  • 移除manage中所有quartz以及bean模块依赖
  • 将bean模块移入task服务中,并且将manage中的quartz任务API迁移至task服务中
  • manage服务通过feign调用task接口继续向前端提供任务维护接口

架构简图对比

旧架构: [Manage] --> (依赖Quartz+Bean) [Task] --> (依赖Quartz+Bean) Bean模块独立

新架构: [Manage] --> [Feign] --> [Task] Task内部包含Bean模块

迁移问题

理论上来讲此次调整只需要将bean中的代码直接放到task服务中,并且将manage中的相关依赖删除掉即可。不出意外的话意外就出现了,运行日志中提示quartz中Properties的org.quartz.jobStore.class配置:org.springframework.scheduling.quartz.JobStoreTX 不存在

image.png

通过排查发现对应的JobStoreTX类,但是我并没有修改quartz依赖的版本,也就是说之前设置的配置有可能全部没有起作用,由于quartz的配置通过spring bean的方式进行设置并没有发现,具体配置如下:

@Configuration
@Import(DataSourceConfiguration.class)
public class QuartzConfiguration implements ApplicationContextAware {

    private static final Logger logger = LoggerFactory.getLogger(QuartzConfiguration.class);


    // =================调整架构后新增配置=================
    private ApplicationContext applicationContext;

    @Resource
    private Scheduler scheduler;

    @Override
    public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @EventListener
    public void onApplicationEvent(@Nonnull WebServerInitializedEvent event) {
        this.loadQuartzJobListener();
        this.loadQuartzTriggerListener();
        this.loadQuartzJobBean();
    }

    private void loadQuartzJobListener() {
        Map<String, QuartzJobListenerTemplate> jobListenerMap =
            applicationContext.getBeansOfType(QuartzJobListenerTemplate.class);

        jobListenerMap.forEach((k, v) -> {
            try {
                scheduler.getListenerManager().addJobListener(v);
            } catch (Exception e) {
                logger.error("Initialization QuartzJobListener error", e);
            }
        });
    }

    private void loadQuartzTriggerListener() {
        Map<String, QuartzTriggerListenerTemplate> triggerListenerMap =
            applicationContext.getBeansOfType(QuartzTriggerListenerTemplate.class);

        triggerListenerMap.forEach((k, v) -> {
            try {
                scheduler.getListenerManager().addTriggerListener(v);
            } catch (Exception e) {
                logger.error("Initialization QuartzTriggerListener error", e);
            }
        });
    }

    private void loadQuartzJobBean() {
        Map<String, Object> quartzJobMap = applicationContext.getBeansWithAnnotation(QuartzJob.class);
        Map<String, String> quatrzBeanMap = MiddlewareDataManage.getInstance().getQuartzBeanMap();
        quartzJobMap.forEach((k, v) -> quatrzBeanMap.put(k, v.getClass().getName()));
    }

    // =================调整架构前的原配置=================

    @Autowired
    @Qualifier(DataSourceConfiguration.DS_BEAN_NAME)
    private DataSource dataSource;

    @Autowired
    @Qualifier(DataSourceConfiguration.TRAN_MGR_BEAN_NAME)
    private DataSourceTransactionManager dataSourceTransactionManager;

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        schedulerFactoryBean.setAutoStartup(true);
        schedulerFactoryBean.setStartupDelay(1);
        schedulerFactoryBean.setOverwriteExistingJobs(true);
        schedulerFactoryBean.setJobFactory(springBeanJobFactory());
        schedulerFactoryBean.setDataSource(dataSource);
        schedulerFactoryBean.setTransactionManager(dataSourceTransactionManager);
        schedulerFactoryBean.setQuartzProperties(quartzProperties());
        return schedulerFactoryBean;
    }

    @Bean
    public SpringBeanJobFactory springBeanJobFactory() {
        // 必须指定JobFactory,否则Job类中的Service无法注入
        return new SpringBeanJobFactory();
    }

    @Bean
    public Properties quartzProperties() {
        Properties properties = new Properties();
        properties.setProperty("org.quartz.scheduler.instanceName", "flight-monitor-task-service");
        properties.setProperty("org.quartz.scheduler.instanceId", "AUTO");
        properties.setProperty("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore");
        properties.setProperty("org.quartz.jobStore.driverDelegateClass",
            "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
        properties.setProperty("org.quartz.jobStore.tablePrefix", "qrtz_");
        properties.setProperty("org.quartz.jobStore.isClustered", "true");
        properties.setProperty("org.quartz.jobStore.misfireThreshold", "2000");
        properties.setProperty("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
        properties.setProperty("org.quartz.jobStore.clusterCheckinInterval", "15000");
        properties.setProperty("org.quartz.jobStore.useProperties", "false");

        properties.setProperty("org.quartz.threadPool.threadCount", "30");
        properties.setProperty("org.quartz.threadPool.threadPriority", "5");
        return properties;
    }
}

查询quartz的文档发现JobStoreTX类是低版本所用的,我们项目中的版本为2.x应该使用LocalDataSourceJobStore.class,调整后继续报错提示:No local DataSource found for configuration - 'dataSource' property must be set on SchedulerFactoryBean image.png

在上面给出的配置中可以看到我已经在schedulerFactoryBean方法中设置了DataSource以及dataSourceTransactionManager,但是为什么还提示数据源为空呢?debug后发现确实为null,可以确认的是在迁移前配置是可用的,但是增加监听器的相关配置后却出现了数据源无法获取的问题,应该是配置类增加了ApplicationContextAware实现影响了spring bean依赖的加载顺序 image.png 将其改为setter方式进行注册,依然为null image.png 于是我又修改为通过方法参数进行注入,发现成功运行图中的null是数据源的线程池name为空,原因是当前线程池还未完全初始化完成。 image.png

遗留问题解答补充

程序虽然启动起来了但是仍然遗留了两个问题

  1. 为什么架构调整前quartz版本没有变化的情况下,旧架构JobStoreTX配置有效?

    于是回到之前的配置重新dubug排查,配置如下 image.png 进入到SchedulerFactoryBean类中发现,定位到afterPropertiesSet方法中,其中的对Scheduler实例进行初始化 image.png 进入prepareSchedulerFactory方法的initSchedulerFactory方法 image.png 其中关键代码为此处的数据源判断,如果数据源不为空会自动将org.quartz.jobStore.class配置设置为LocalDataSourceJobStore类,也就是说调整架构之前的写法一直为其作用,走的默认值 image.png

  • Spring的SchedulerFactoryBean在检测到setDataSource()被调用时
  • 自动将org.quartz.jobStore.class设置为LocalDataSourceJobStore
  • 这个行为优先级高于配置文件中的显式设置
  • 实际运行时使用的是正确的JobStore实现
  • 导致旧配置中的错误值JobStoreTX被静默覆盖
  1. ApplicationContextAware的影响

Spring容器初始化Bean时会通过 BeanPostProcessor 处理 ApplicationContextAware 接口,强制调用 setApplicationContext() 方法。这会使得 Configuration 类在更早的阶段被初始化(早于普通 Bean 的依赖注入阶段)。此时,其他 Bean 可能尚未完成初始化,导致通过 Setter 或字段注入(@Autowired)的方式无法获取到正确的依赖值。 而通过 @Bean 方法参数注入是延迟解析的。Spring 在调用 @Bean 方法时,会实时解析参数对应的 Bean,此时所有依赖的 Bean 已经初始化完成。