最近在写Job应用,打算整理一下。
可以在http://www.quartz-scheduler.org/站点下载 Quartz 的发布版本及其源代码。
先解释一下什么叫调度:(来自维基百科)调度,在计算机中是分配工作所需资源的方法,这里的资源可以指虚拟的计算资源,如线程、进程或数据流;也可以指硬件资源,如处理器、网络连接或扩展卡。进行调度工作的程序叫调度器。
分布式与集群:(引用)小饭店原来只有一个厨师,切菜洗菜备料炒菜全干。后来客人多了,厨房一个厨师忙不过来,又请了个厨师,两个厨师都能炒一样的菜,这两个厨师的关系是集群。为了让厨师专心炒菜,把菜做到极致,又请了个配菜师负责切菜,备菜,备料,厨师和配菜师的关系是分布式,一个配菜师也忙不过来了,又请了个配菜师,两个配菜师关系是集群。
可参考这篇文章:https://blog.csdn.net/u013142781/article/details/51307229
Quartz基本概念:
它是一个优秀的开源调度框架,有强大的调度功能,应用方式灵活(支持任务和调度的各种组合方式,支持调度数据的多种储存方式),有分布式和集群能力。而且,Quartz是Spring默认的调度框架,很容易与Spring集成实现灵活可配置的调度功能。
Quartz调度核心元素:
- Scheduler:任务调度器,实际执行任务调度的控制器,在Spring中通过SchedulerFactoryBean封装起来。
- Trigger:触发器,用于定义任务调度的时间规则,有SimpleTrigger,CronTrigger,DateIntervalTrigger和NthIncludedDayTrigger,其中CronTrigger用的比较多。
- Calendar:它是一些日历特定时间点的集合。一个trigger可以包含多个Calendar,以便排除或包含某些时间点。
- JobDetail:描述Job实现类及其它相关的静态信息,如Job名字、关联监听器等信息。在spring中有JobDetailFactoryBean和 MethodInvokingJobDetailFactoryBean两种实现,如果任务调度只需要执行某个类的某个方法,就可以通过MethodInvokingJobDetailFactoryBean来调用。
- Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中。实现Job接口的任务,默认是无状态的,若要将Job设置成有状态的,在quartz中是给实现的Job添加@DisallowConcurrentExecution注解(以前是实现StatefulJob接口,现在已被Deprecated),在与spring结合中可以在spring配置文件的job detail中配置concurrent参数。
Quartz集群配置:
主要通过数据库表来感知其他的应用的,各个节点之间并没有直接的通信。只有使用持久的JobStore才能完成Quartz集群。具体的表不加描述。
Quartz基本原理:
主要就是三个核心元素:Scheduler、trigger、job,Scheduler是实际执行调度的控制器,trigger和Job是任务调度的元数据。
Scheduler:主要有三种RemoteMBeanScheduler, RemoteScheduler 和 StdScheduler。主要由Scheduler工厂创建DirectSchedulerFactory 或者 StdSchedulerFactory。 第二种工厂 StdSchedulerFactory 使用较多。
trigger:用于定义调度时间,按照什么时间规则去执行任务。Quartz 中主要提供了四种类型的 trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和 NthIncludedDayTrigger。
Job:表示被调度的任务。主要有两种类型的 job:无状态的(stateless)和有状态的(stateful)。对于同一个 trigger 来说,有状态的 job 不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job 主要有两种属性:volatility 和 durability,其中 volatility 表示任务是否被持久化到数据库存储,而 durability 表示在没有 trigger 关联的时候任务是否被保留。两者都是在值为 true 的时候任务被持久化或保留。一个 job 可以被多个 trigger 关联,但是一个 trigger 只能关联一个 job。
元素之间关系图
工作原理:
- IOC容器初始化时(用Spring与Quartz结合的)会创建并初始化Quartz线程池(TreadPool),并启动它。刚启动时线程池中每个线程都处于等待状态,等待外界给他分配Runnable(持有作业对象的线程)。
- 初始化并启动Quartz的主线程(QuartzSchedulerThread),该线程自启动后就会等待外界的信号量开始工作。外界给出工作信号量之后,该主线程的run方法才实质上开始工作。run中会获取JobStore中下一次要触发的作业,拿到之后会一直等待到该作业的真正触发时间,然后将该作业包装成一个JobRunShell对象(该对象实现了Runnable接口,其实看是上面TreadPool中等待外界分配给他的Runnable),然后将刚创建的JobRunShell交给线程池,由线程池负责执行作业。
- 线程池收到Runnable后,从线程池一个线程启动Runnable,然后将该线程回收至空闲线程中。
- JobRunShell对象的run方法就是最终通过反射调用作业的地方。
企业应用实例:
第一步:首先配置文件,进行SchedulerFactoryBean初始化,进行Scheduler、trigger、job的创建。
@Configuration @ConditionalOnProperty(name = "quartz.enable", havingValue = "true") public class JobConfig{ @Autowired private DataSource dataSource; //注册集群调度任务Scheduler @Bean public SchedulerFactoryBean schedulerFactoryBean() throws IOException { SchedulerFactoryBean scheduler = new SchedulerFactoryBean(); scheduler.setDataSource(dataSource); // 数据库 scheduler.setJobFactory(jobFactory()); //作业实例和自动装配依赖项的作业工厂 scheduler.setSchedulerName("Scheduler"); //quartz.properties 文件配置 scheduler.setQuartzProperties(quartzProperties("/quartz.properties")); scheduler.setTriggers(requestEventExecuteTrigger.getObject()); //可以多个trigger return scheduler; } //创建Trigger @Bean(name = "requestEventExecuteTrigger") public CronTriggerFactoryBean requestEventExecuteTrigger() { CronTriggerFactoryBean factoryBean = new CronTriggerFactoryBean(); factoryBean.setJobDetail(requestEventExecuteJob().getObject()); factoryBean.setGroup(group); factoryBean.setName("requestEventExecuteTrigger"); factoryBean.setCronExpression("0 0/5 * * * ? "); return factoryBean; } //创建JobDetail @Bean(name = "requestEventExecuteJob") public JobDetailFactoryBean requestEventExecuteJob() { JobDetailFactoryBean factoryBean = new JobDetailFactoryBean(); factoryBean.setJobClass(RequestEventExecuteJob.class); //具体Job实现 factoryBean.setGroup(group); factoryBean.setName("requestEventExecuteJob"); factoryBean.setDurability(true); return factoryBean; } //创建作业实例和自动装配依赖项的作业工厂 @Bean public AutowireableJobFactory jobFactory() { return new AutowireableJobFactory(); } }
第二步:配置quartz.properties文件
- 配置主调度器设置
- 配置ThreadPool
- 配置全局监听器
- 配置Scheduler调度程序插件
- 配置RMI //远程服务器调用
- 配置RAMJobStore //内存存储机制实现类
- 配置JDBC-JobStoreTX //受应用容器管理事物的数据库存储实现类
- 配置JDBC-JobStoreCMT //不受应用容器事物管理的数据库存储实现类
- 配置DataSource
- 用JDBC-JobStore配置集群
- 配置TerracottaJobStore
一般会写两种配置文件quartz.properties和quartz-memory.properties,两个文件的唯一区别就是:
quartz.properties:任务存储的顶层接口org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
(JobStoreTX使用数据库存储,TX是事物的意思,不管我们的Service服务本身业务代码是否执行成功,只要代码中调用了Quartz API的数据库操作,那任务状态就永久持久化了,就算业务代码抛出运行时异常任务状态也不会回滚到之前的状态。在这里就需要配置org.quartz.jobStore.dataSource属性。JobStoreCMT 也是使用数据库存储,不管是业务代码还是Quartz内部抛出异常,Service服务内的所有数据操作都会回滚到原始状态。JobStoreCMT和JobStoreTX最大的区别是JobStoreCMT需要配置两个数据源,一个是受应用容器管理的数据源,还有一个是不受应用容器管理的数据源。)quartz-memory.properties:任务存储的顶层接口org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
(本地测试一般用这个,使用RAMJobStore当Quartz程序或应用程序停止了,伴随在内存中的数据也会被回收,任务等数据就永久丢失了,优点是任务的存储和读取的速度极快,使用内存存储时,注意配置文件中只需要保留基本的线程池配置和jobStore的实现类等几个简单的属性就行。如果使用了实现类中没有的属性,启动的时候会报错)注解:
- 什么情况下使用RAMJobStore?有些任务是随着项目启动而启动的,就算项目关闭或系统宕机,那也没关系,因为项目重新启动后此任务又会随之启动。如果项目中只存在这类任务,那么就可以用内存存储。
- 什么情况下使用JobStoreTX?如果在一个Service服务中需要创建一个Job,那么请把创建Job的代码编写在服务代码的最后面,确保业务代码运行成功并且没有抛异常再去启动Job,如果启动Job失败的时候请抛出一个运行时异常使业务代码进行回滚。
- 什么情况下使用JobStoreCMT?看情况,一般很少用。
第三步:创建作业实例和自动装配依赖项的作业工厂(jobFactory)
/** * The job factory for creating job instance and autowiring dependencies. */ public class AutowireableJobFactory extends AdaptableJobFactory{ private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } /** * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#autowireBean(Object) */ @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { Object jobObject = super.createJobInstance(bundle); applicationContext.getAutowireCapableBeanFactory().autowireBean(jobObject); return jobObject; } }
第四步:使用Quartz有两种方法:extends QuartzJobBean和MethodInvokingJobDetailFactoryBean。这里主要介绍第一种方法。
public abstract class CronJob extends QuartzJobBean { private static Logger log = LoggerFactory.getLogger(CronJob.class); private static final String log4jLogNameKey = "jobName"; @Override public void executeInternal(JobExecutionContext context) { String name = jobName(); if (context != null) { name = context.getJobDetail().getKey().getName(); } try { ThreadContext.put(log4jLogNameKey, name); log.info("{}:start", name); run(context); log.info("{}:end", name); } catch (Throwable throwable) { log.error("JOB {} execute error : {}", name, StackTraceUtil.getStackTraceAsString(throwable)); } finally { ThreadContext.remove(log4jLogNameKey); } } protected String jobName() { return getClass().getSimpleName(); } public abstract void run(JobExecutionContext context) throws JobExecutionException; }
第五步:具体Job实现service
@Slf4j @DisallowConcurrentExecution public class RequestEventExecuteJob extends CronJob{ public void run(JobExecutionContext context) throws JobExecutionException { //service } }
至于QuartzJob的线程是如何实现的,没有深入了解,暂时不写。以后再补。有写的不对的地方后续修改。