Dubbo学习系列之十二(Quartz任务调度)

Quartz词义为"石英"水晶,而后聪明的人类利用它发明了石英手表,因石英晶体在受到电流影响时,它会产生规律的振动,因而,这种时间上的规律,也被应用到了软件界,来命名了一款任务调度框架--Quartz。现实软件逻辑中,周期任务有着普遍的存在,如定时刷新配置信息,按期盘点库存,定时收发邮件等,至于定时任务处理,也有Spring的ScheduledThreadPool,还有基于注解@Scheduled的方式,ScheduledThreadPool主要是基于相对时间,不方便控制,而@Scheduled则会致使连锁错误,因此咱们来用下Quartz,看看有啥优点。ios

工具:Idea201902/JDK11/ZK3.5.5/Gradle5.4.1/RabbitMQ3.7.13/Mysql8.0.11/Lombok0.26/Erlang21.2/postman7.5.0/Redis3.2/RocketMQ4.5.2/Sentinel1.6.3/SpringBoot2.1.6/Quartz2.3.1/Nacos1.1.3git

难度: 新手--战士--老兵--大师github

目标:1.使用Quartz实现物流订单按期延误检查;sql

步骤:数据库

1.总体框架依旧,多模块微服务框架商城系统,一个共享模块,多个功能模块,具体见项目代码结构。并发

2.按照惯例,先上几个Quartz的核心概念的菜:app

  • Job-任务:一个接口,只有一个execute方法,使用时该方法内容即为须要执行的任务逻辑,还有个关联的JobDetail接口,注意这二者并非继承关系,Quartz在每次执行Job时,都从新建立一个Job实例,因此它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时经过newInstance()的反射机制实例化Job。所以须要经过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色,Quartz源码中描述二者关系:"Quartz不会保存一个Job接口的实例,但能够经过JobDetail来定义一个实例",JobDetail包含一个getJobClass()得到Job实例字节码的方法;框架

  • Trigger-触发器:手枪的扳机,何时发射,就看何时触发了该类设定的条件,可自由定义触发规则,多个触发器可做用于一个Job,但一个触发器只可做用于一个Job;异步

  • Scheduler-调度器:表明一个Quartz的独立运行容器,Trigger和JobDetail能够注册到Scheduler中,二者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须惟一,JobDetail的组和名称也必须惟一(但能够和Trigger的组和名称相同,由于它们是不一样类型的);async

3.先作个简单的上手小菜,定义一个HelloJob类,内容就是打印HelloWrold:

 

public class HelloJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println(System.currentTimeMillis()+"helloWorld"); } }

再直接使用main入口,定义jobDetail -->Trigger-->Scheduler,能够看到这里并无直接使用HelloJob类,而是以Class形式放入JobDetail 中,很明显使用的就是Java反射机制了,代码清晰简单,不解释了。

public class ScheduledTaskMain { public static void main(String[] args) throws SchedulerException { /*建立一个jobDetail的实例,将该实例与HelloJob Class绑定*/ JobDetail jobDetail = JobBuilder.newJob(HelloJob.class).withIdentity("HelloJob").build(); /*建立一个触发器,每2秒执行一次任务,一直持续下去*/ SimpleTrigger cronTrigger=  TriggerBuilder.newTrigger().withIdentity("HelloTrigger").startNow() .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2).repeatForever()).build(); /*建立schedule实例*/ StdSchedulerFactory factory = new StdSchedulerFactory(); Scheduler scheduler = factory.getScheduler(); /*将Job和trigger放入Scheduler容器*/ scheduler.scheduleJob(jobDetail,cronTrigger); scheduler.start(); } }

4.愉快地跑一个:

 

 

从输出能够看到Quartz内部系列对象的建立过程,并创建了10线程的ThreadPool,最后执行了HelloWorld任务。

5.固然,咱们的主菜不多是作HelloWorld任务,那还不简单!改起来!为了试验方便,我只改动logistic模块,新建一个Job类:com.biao.mall.logistic.schedule.HelloJob2,直接注入SpringBean任务,这个任务就是按期检查过时未发的物流单,具体见deliveryService.checkDelayed()方法:

@Component @DisallowConcurrentExecution //标识这个任务是串行执行,不是并发执行
public class HelloJob2 implements Job, Serializable { @Autowired private DubboDeliveryService deliveryService; /*经测试,下面这种构造方法注入deliveryService的方式会致使NPE!*/
/* @Autowired public HelloJob2(DubboDeliveryService deliveryService){ this.deliveryService = deliveryService; } public HelloJob2(){} */ @Override public void execute(JobExecutionContext context) throws JobExecutionException { int num = deliveryService.checkDelayed(); System.out.println("delayed num is >>> "+ num); } }

com.biao.mall.logistic.impl.DubboDeliveryServiceImpl中,实现deliveryService.checkDelayed()方法:

//检查延误未发的物流单,
    /*仅供逻辑测试,直接查找出全部10天以前的订单,生产逻辑确定比这复杂*/ @Override public int checkDelayed() { QueryWrapper qw = new QueryWrapper(); LocalDateTime timeNow = LocalDateTime.now(ZoneId.systemDefault()); qw.lt(true,"gmt_create",timeNow.minusDays(10L)); List<DubboDeliveryEntity> list = deliveryDao.selectList(qw); return Objects.isNull(list)? 0: list.size(); }

再将ScheduledTaskMain中替换为Job2,运行结果NPE:

 

 

这就有点让人失望了,缘由在哪?这是由于经过实现Job接口的方式来建立定时任务,这个类在实例化时是被Quartz实例化的,同时没有注入到Spring中。而自定义的Service是Spring容器管理的,所以就致使了被Spring所管理的Bean不能被自动注入进来,Quartz也没法感知自定义的ServiceBean的存在!

6.关于@DisallowConcurrentExecution 注解:即该Job不并发执行,好比当前一个Job未执行完,而下一个Job也知足Trigger条件,此时就会被阻塞。(详细解释请见后记)

7.该NPE解决思路,就是要将Scheduler也归入Spring容器管理, 先定义com.biao.mall.logistic.schedule.MyJobFactory,继承自AdaptableJobFactory:

  • AdaptableJobFactory 是一个支持Runnable和Job对象的工厂,实现了JobFactory接口;

  • TriggerFiredBundle是一个接收从JobStore到QuartzSchedulerThread执行时数据的类;

@Component public class MyJobFactory extends AdaptableJobFactory { /** * AutowireCapableBeanFactory接口是BeanFactory的子类 * 能够链接和填充那些生命周期不被Spring管理的已存在的bean实例 */
    private AutowireCapableBeanFactory capableBeanFactory; @Autowired public MyJobFactory(AutowireCapableBeanFactory capableBeanFactory){ this.capableBeanFactory = capableBeanFactory; } @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception{ //调用父类方法
        Object jobInstance = super.createJobInstance(bundle); //进行注入(Spring接管该Bean)
 capableBeanFactory.autowireBean(jobInstance); return jobInstance; } }

8.再定义一个com.biao.mall.logistic.schedule.QuartzConf配置类,能够经过SchedulerFactoryBean这个桥梁来完成ApplicationContext和SchedulerContext关联,以下,再运行程序即正常执行!

@Configuration public class QuartzConfig { //不推荐这里注解@Autowired,使用构造函数注入
    private MyJobFactory myJobFactory; @Autowired public QuartzConfig(MyJobFactory myJobFactory){ this.myJobFactory = myJobFactory; } @Bean(name = "factoryBean") public SchedulerFactoryBean schedulerFactoryBean(){ // Spring提供SchedulerFactoryBean为Scheduler提供配置信息,并被Spring容器管理其生命周期
        SchedulerFactoryBean factoryBean = new SchedulerFactoryBean(); factoryBean.setOverwriteExistingJobs(true); //设置是否自动启动
        factoryBean.setAutoStartup(false); //设置系统启动后,Starting Quartz Scheduler的延迟时间
        factoryBean.setStartupDelay(30); // 设置自定义Job Factory,用于Spring管理Job bean
 factoryBean.setJobFactory(myJobFactory); return factoryBean; } @Bean(name = "myScheduler") public Scheduler getScheduler(){ Scheduler scheduler = schedulerFactoryBean().getScheduler(); return scheduler; } }

来看点SchedulerFactoryBean的源码(部分):

/** * {@link FactoryBean} that creates and configures a Quartz {@link org.quartz.Scheduler}, * manages its lifecycle as part of the Spring application context, and exposes the * Scheduler as bean reference for dependency injection. * * <p>Allows registration of JobDetails, Calendars and Triggers, automatically * starting the scheduler on initialization and shutting it down on destruction. * In scenarios that just require static registration of jobs at startup, there * is no need to access the Scheduler instance itself in application code. // 代码省略部分 ... ... */
 public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBean<Scheduler>, BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean, SmartLifecycle { public static final String PROP_THREAD_COUNT = "org.quartz.threadPool.threadCount"; public static final int DEFAULT_THREAD_COUNT = 10; private static final ThreadLocal<ResourceLoader> configTimeResourceLoaderHolder = new ThreadLocal<>(); private static final ThreadLocal<Executor> configTimeTaskExecutorHolder = new ThreadLocal<>(); private static final ThreadLocal<DataSource> configTimeDataSourceHolder = new ThreadLocal<>(); private static final ThreadLocal<DataSource> configTimeNonTransactionalDataSourceHolder = new ThreadLocal<>(); // 代码省略部分 ... ... //--------------------------------------------------------------------- // 实现接口InitializingBean,即SpringBean生命周期中的afterPropertiesSet()方法,dataSource是持久化属性,
 @Override public void afterPropertiesSet() throws Exception { if (this.dataSource == null && this.nonTransactionalDataSource != null) { this.dataSource = this.nonTransactionalDataSource; } if (this.applicationContext != null && this.resourceLoader == null) { this.resourceLoader = this.applicationContext; } // 初始化Scheduler实例,将Jobs/Triggers注册
    this.scheduler = prepareScheduler(prepareSchedulerFactory()); try { registerListeners(); registerJobsAndTriggers(); } catch (Exception ex) { try { this.scheduler.shutdown(true); } catch (Exception ex2) { logger.debug("Scheduler shutdown exception after registration failure", ex2); } throw ex; } } // 代码省略部分 ... ...
  /** * 初始化当前的SchedulerFactory, 应用本地定义的属性值 * @param参数schedulerFactory为须要初始化的对象 */
  private void initSchedulerFactory(StdSchedulerFactory schedulerFactory) throws SchedulerException, IOException { Properties mergedProps = new Properties(); if (this.resourceLoader != null) { mergedProps.setProperty(StdSchedulerFactory.PROP_SCHED_CLASS_LOAD_HELPER_CLASS, ResourceLoaderClassLoadHelper.class.getName()); } if (this.taskExecutor != null) { mergedProps.setProperty(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, LocalTaskExecutorThreadPool.class.getName()); } else { // 设置必要的默认属性,Quartz会使用显式属性设置覆盖默认属性 
      mergedProps.setProperty(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, SimpleThreadPool.class.getName()); mergedProps.setProperty(PROP_THREAD_COUNT, Integer.toString(DEFAULT_THREAD_COUNT)); } if (this.configLocation != null) { if (logger.isInfoEnabled()) { logger.info("Loading Quartz config from [" + this.configLocation + "]"); } PropertiesLoaderUtils.fillProperties(mergedProps, this.configLocation); } CollectionUtils.mergePropertiesIntoMap(this.quartzProperties, mergedProps); if (this.dataSource != null) { mergedProps.put(StdSchedulerFactory.PROP_JOB_STORE_CLASS, LocalDataSourceJobStore.class.getName()); } if (this.schedulerName != null) { mergedProps.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, this.schedulerName); } schedulerFactory.initialize(mergedProps); } // 代码省略部分 ... ...
  /** * 根据给定的factory 和scheduler name生成Scheduler实例,由afterPropertiesSet调用 缺省实现将触发SchedulerFactory的getScheduler方法 * @param 参数schedulerFactory生产Scheduler的工厂 * @param schedulerName the name of the scheduler to create * @return the Scheduler instance * @throws SchedulerException if thrown by Quartz methods * @see #afterPropertiesSet * @see org.quartz.SchedulerFactory#getScheduler */
  protected Scheduler createScheduler(SchedulerFactory schedulerFactory, @Nullable String schedulerName) throws SchedulerException { // Override thread context ClassLoader to work around native Quartz ClassLoadHelper loading.
    Thread currentThread = Thread.currentThread(); ClassLoader threadContextClassLoader = currentThread.getContextClassLoader(); boolean overrideClassLoader = (this.resourceLoader != null &&
        this.resourceLoader.getClassLoader() != threadContextClassLoader); if (overrideClassLoader) { currentThread.setContextClassLoader(this.resourceLoader.getClassLoader()); } try { SchedulerRepository repository = SchedulerRepository.getInstance(); synchronized (repository) { Scheduler existingScheduler = (schedulerName != null ? repository.lookup(schedulerName) : null); Scheduler newScheduler = schedulerFactory.getScheduler(); if (newScheduler == existingScheduler) { throw new IllegalStateException("Active Scheduler of name ' " + schedulerName + " ' already registered " +
              "in Quartz SchedulerRepository. Cannot create a new Spring-managed Scheduler of the same name!"); } if (!this.exposeSchedulerInRepository) { // Need to remove it in this case, since Quartz shares the Scheduler instance by default!
 SchedulerRepository.getInstance().remove(newScheduler.getSchedulerName()); } return newScheduler; } } finally { if (overrideClassLoader) { // 重置初始的线程ClassLoader.
 currentThread.setContextClassLoader(threadContextClassLoader); } } } /** * 将指定的或当前的ApplicationContext暴露给Quartz SchedulerContext. */
  private void populateSchedulerContext(Scheduler scheduler) throws SchedulerException { // 将对象放入Scheduler context.
    if (this.schedulerContextMap != null) { scheduler.getContext().putAll(this.schedulerContextMap); } // 在Scheduler context中注册ApplicationContext.
    if (this.applicationContextSchedulerContextKey != null) { if (this.applicationContext == null) { throw new IllegalStateException( "SchedulerFactoryBean needs to be set up in an ApplicationContext " +
          "to be able to handle an ' applicationContextSchedulerContextKey'"); } scheduler.getContext().put(this.applicationContextSchedulerContextKey, this.applicationContext); } } /** * 根据startupDelay设置启动Scheduler,注意这里是异步启动 * @param scheduler the Scheduler to start * @param startupDelay the number of seconds to wait before starting * the Scheduler asynchronously */
  protected void startScheduler(final Scheduler scheduler, final int startupDelay) throws SchedulerException { if (startupDelay <= 0) { logger.info("Starting Quartz Scheduler now"); scheduler.start(); } else { if (logger.isInfoEnabled()) { logger.info("Will start Quartz Scheduler [" + scheduler.getSchedulerName() +
            "] in " + startupDelay + " seconds"); } // 因这里明确须要一个守护线程,因此不使用Quartz的startDelayed方法, // 这样当其余线程所有终止时,应用就终止,JVM也会退出
      Thread schedulerThread = new Thread() { @Override public void run() { try { Thread.sleep(TimeUnit.SECONDS.toMillis(startupDelay)); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); // 简单处理
 } if (logger.isInfoEnabled()) { logger.info("Starting Quartz Scheduler now, after delay of " + startupDelay + " seconds"); } try { scheduler.start(); } catch (SchedulerException ex) { throw new SchedulingException("Could not start Quartz Scheduler after delay", ex); } } }; schedulerThread.setName("Quartz Scheduler [" + scheduler.getSchedulerName() + "]"); schedulerThread.setDaemon(true); // 指定thread为deamon类型
 schedulerThread.start(); } } // 代码省略部分 ... ...
    }
  • 类描述的两段:此类做为Bean工厂产生并配置Scheduler,并将其归入Spring应用上下文中的Bean生命周期管理;提供JobDetails, Calendars and Triggers的注册,在应用启动时,自动启动Scheduler,在应用关闭时,自动中止Scheduler;

  • 实现了ApplicationContextAware接口,即能被ApplicationContext接管,因此这以后使用ApplicationContext.getBean()也可取得Scheduler;

  • 多个static final型的类变量,其DEFAULT_THREAD_COUNT指定了Quartz后台线程池大小,几个ThreadLocal用于保存线程Context,这也是Job能保持独立性的关键基础;

  • initSchedulerFactory方法初始化当前的SchedulerFactory, 应用本地定义的属性值,好比指定线程池大小;

  • afterPropertiesSet方法,经过实现接口InitializingBean,使用SpringBean生命周期中的afterPropertiesSet()方法来设置Scheduler;

  • createScheduler建立Scheduler,并交给Spring来接管,并对“同名Scheduler”异常作处理;

  • startScheduler异步守护线程方式启动Scheduler;

总结,Scheduler就是一个容器,有本身的内部对象和上下文,属于重量级对象,理解上能够类比SpringContext,可以使用scheduler.getContext()取得上下文信息。

9.Quartz的任务管理,经过Scheduler能够start、pause、resume、stop,addJob和deleteJob等重要方法来调度Job的执行。这里只须要注意一点,若是stop以后,就没法再直接start,必须重启应用,不知道这个是否属于bug。

10.定义一个com.biao.mall.logistic.schedule.QuartzService类,来封装这些方法:

@Service public class QuartzService { private final String groupName = "group1"; @Autowired @Qualifier(value = "factoryBean") private SchedulerFactoryBean factoryBean; //如下方式也能够获取bean // Scheduler scheduler = SpringUtil.getBean("myScheduler");
 @Autowired @Qualifier(value = "myScheduler") private Scheduler scheduler; // 启动 Scheduler
    public void startScheduleJobs() throws SchedulerException { if (this.scheduler.isStarted()){ return; } this.setCheckScheduler(scheduler); this.scheduler.start(); } // 中止 Scheduler
    public void stopScheduleJobs() { scheduler = factoryBean.getScheduler(); try { if (!scheduler.isShutdown()){ scheduler.shutdown(); } } catch (SchedulerException e) { e.printStackTrace(); } } // 添加 Job 并替换
    public void addJobandReplace(){ //打印hellowrold的job
        JobDetail jobDetail = JobBuilder.newJob(HelloJob.class).withIdentity("HelloJob").storeDurably(true).build(); // 第二个参数为replace,是否替换存在的同名job // jobDetail必须是durable属性,表示任务完成以后是否依然保留到数据库,且无定义关联的trigger
        try { this.scheduler.addJob(jobDetail,true); } catch (SchedulerException e) { e.printStackTrace(); } } // 添加 Job 不替换
    public void addJobwithoutReplace(){ //打印hellowrold的job
        JobDetail jobDetail = JobBuilder.newJob(HelloJob.class).withIdentity("HelloJob").storeDurably(true).build(); // 第二个参数为replace,是否替换存在的同名job
        try { this.scheduler.addJob(jobDetail,false); } catch (SchedulerException e) { e.printStackTrace(); } } // 暂停全部 Job,还可指定具体的Job
    public void pauseScheduler(){ try { this.scheduler.pauseAll(); } catch (SchedulerException e) { e.printStackTrace(); } } // 恢复并继续全部Job执行,还可指定具体的Job
    public void resumeJobs(){ try { this.scheduler.resumeAll(); } catch (SchedulerException e) { e.printStackTrace(); } } //配置一个自定义的scheduler
    private void setCheckScheduler(Scheduler scheduler) throws SchedulerException { //添加HelloJob2做为任务内容
        JobDetail jobDetail = JobBuilder.newJob(HelloJob2.class) .withIdentity("job1",groupName).build(); //cron表达式制定触发规则,每10秒执行一次
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ?"); CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("trigger1",groupName) .withSchedule(scheduleBuilder).build(); scheduler.scheduleJob(jobDetail,cronTrigger); } }

还有很多其余方法,就不一一列举了,注意下:

  • setCheckScheduler()中加入自定义的JobDetail和Trigger,并注册进Scheduler容器,若是有多个,就定义多个相似方法加入便可,而后startScheduleJobs()启动Scheduler;

  • cron表达式:用一个cron字符串表示一个时间规则;

11.测试:启动ZK-->Nacos-->RocketMQ-->business-->logistic, 写几个简陋的controller方法:

@RestController public class DubboDeliveryController { // 代码省略部分 ... ...
 @RequestMapping("/delivery/start") public String start() throws SchedulerException { quartzService.startScheduleJobs(); return "startScheduleJobs success"; } @RequestMapping("/delivery/stop") public String stop(){ quartzService.stopScheduleJobs(); return "stopScheduleJobs success"; } @RequestMapping("/delivery/add") public String addJob(){ quartzService.addJobandReplace(); return "addJobandReplace success"; } @RequestMapping("/delivery/pause") public String pauseJob(){ quartzService.pauseScheduler(); return "pauseScheduler success"; } @RequestMapping("/delivery/resume") public String resumeJob(){ quartzService.resumeJobs(); return "resumeJobs success"; } }

 

数据库状况:

 

 

URL给个访问:

 

 

结果:

 

 

12.项目代码地址:其中的day15    https://github.com/xiexiaobiao/dubbo-project.git


后记:

1.Quartz使用FixedThreadPool(固定数线程池)来执行Job,默认数量为10(本例中可经过QuartzConfig修改),此ThreadPool接收Runnable对象,若是并发过大,就阻塞。各线程经过ThreadLocal来保存本身的独立上下文。

2.关于Job并发解释:

Job有一个StatefulJob子接口,表明有状态的任务,该接口是一个没有方法的标签接口,其目的是让Quartz知道任务的类型,以便采用不一样的执行方案。无状态任务在执行时拥有本身的JobDataMap拷贝,对JobDataMap的更改不会影响下次的执行。而有状态任务共享同一个JobDataMap实例,每次任务执行对JobDataMap所作的更改会保存下来,后面的执行能够看到这个更改,也即每次执行任务后都会对后面的执行发生影响。

正由于这个缘由,无状态的Job能够并发执行,而有状态的StatefulJob不能并发执行,这意味着若是前次的StatefulJob尚未执行完毕,下一次的任务将阻塞等待,直到前次任务执行完毕。有状态任务比无状态任务须要考虑更多的因素,程序每每拥有更高的复杂度,所以除非必要,应该尽可能使用无状态的Job。

若是Quartz使用了数据库持久化任务调度信息,无状态的JobDataMap仅会在Scheduler注册任务时保持一次,而有状态任务对应的JobDataMap在每次执行任务后都会进行保存。

3.Quartz支持集群模式和持久化机制,能够写入后台DB进行保存和恢复。请君自行研究。另寻时间我另起一篇。

4.为何Quartz的各个Job执行互不影响?源码注释:

"Note that Quartz instantiates a new Job for each execution, in contrast to Timer which uses a TimerTask instance that is shared between repeated executions. Just JobDetail descriptors are shared."

核心总结即“每次执行Quartz都是实例化一个新的Job”!

5.Cron表达式在线生成,轻轻松松不伤脑:http://cron.qqe2.com/

 

推荐阅读:

相关文章
相关标签/搜索