浅谈Spring Quartz任务调度器

quartz.jpg

1.引言

Quartz是一个开源的任务调度框架。基于定时、定期的策略来执行任务是它的核心功能,比如2018年除夕夜晚上8点发红包或者打开【木子道】公众号,每隔10分钟发1次红包。Quartz有3个核心要素:调度器(Scheduler)、任务(Job)、触发器(Trigger)。Quartz完全使用Java开发,可以集成到各种规模的应用程序中。它能够承载成千上万的任务调度,并且支持集群。它支持将数据存储到数据库中以实现持久化,并支持绝大多数的数据库。它将任务与触发设计为松耦合,即一个任务可以对应多个触发器,这样能够轻松构造出极为复杂的触发策略。

2.Quartz 核心API介绍(版本2.3.0)

Scheduler :与调度器交互的主要接口。
Job:实现该接口组件能被调度程序执行的任务类。
JobDetail:用于定义Job的实例。
Trigger:触发器,定义一个Job如何被调度器执行。
TriggerBuilder:触发器创建器,用于创建触发器Trigger实例。
JobBuilder :用于声明一个任务实例,也可以定义关于该任务的详情比如任务名、组名等,这个声明的实例将会作为一个实际执行的任务。

Scheduler
void start() ; 开始启动Scheduler的线程Trigger。
void shutdown() ;关闭Scheduler的Trigger,并清理与调度程序相关联的所有资源。
boolean isShutdown(); Scheduler是否关闭
Date scheduleJob(JobDetail var1, Trigger var2) ;将该给定添加JobDetail到调度程序,并将给定Trigger与它关联
void triggerJob(JobKey var1, JobDataMap var2) ;触发标识JobDetail(立即执行)。
void pauseJob(JobKey var1) ; 暂停任务
void resumeJob(JobKey var1); 恢复任务
boolean deleteJob(JobKey var1); 删除任务
......
Scheduler由SchedulerFactory创建,并随着shutdown方法的调用而终止。创建后它将可被用来添加、删除或列出Job和Trigger,或执行一些调度相关的工作。Scheduler 创建完成后,处于“待命”模式,并且必须先start()调用其方法才能触发任何Jobs只有通过start()方法启动后它才会真的工作。

Job
void execute(JobExecutionContext var1) throws JobExecutionException;这个接口由代表要执行的“工作”的类来实现。实例Job必须有一个public 无参数的构造函数。

当job的一个trigger被触发后,execute()方法会被scheduler的一个工作线程调用;传递给execute()方法的JobExecutionContext对象中保存着该job运行时的一些信息 ,执行job的scheduler的引用,触发job的trigger的引用,JobDetail对象引用,以及一些其它信息。

JobDetail
JobDataMap getJobDataMap(); 获取实例成员数据
JobBuilder getJobBuilder();获得JobBuilder对象

JobDetail对象是在将job加入scheduler时,由客户端程序(你的程序)创建的。它包含job的各种属性设置,以及用于存储job实例状态信息的JobDataMap。

Trigger
TriggerKey getKey(); 获取触发器唯一标示,同分组key唯一,key= key + 组名称,默认组名称DEFAULT。
JobKey getJobKey();获取任务唯一标示,
JobDataMap getJobDataMap();获取实例数据。
int getPriority();触发优先级,默认是5,同时触发才会比较优先级。

Trigger用于触发Job的执行。当你准备调度一个job时,你创建一个Trigger的实例,然后设置调度相关的属性。Trigger也有一个相关联的JobDataMap,用于给Job传递一些触发相关的参数。Quartz自带了各种不同类型的Trigger,最常用的主要是SimpleTrigger和CronTrigger。

CronTrigger
CronTrigger通常比SimpleTrigger更有用,如果您需要一个基于类似日历的概念重复出现的工作调度计划,而不是SimpleTrigger的精确指定时间间隔。

使用CronTrigger,您可以在每周一上午九点打开我的微信公众号【木子道】,查阅相关技术分享文章。还可以每周五晚8点给我回复。

即使如此,就像SimpleTrigger一样,CronTrigger有一个startTime,指定调度何时生效,还有一个(可选)endTime,指定何时应该停止调度。

Cron表达式
Cron-Expressions用于配置CronTrigger的实例。Cron-Expressions是由七个子表达式组成的字符串,它们描述了计划的各个细节。这些子表达式用空格分隔,表示:
秒 分钟 小时 日 月 星期 年份(可选)
一个完整的cron-expression的例子是字符串“0 0 0 1 * ?“ - 这意味着”每个每月1日00:00:00 发布统计上个月数据“。

单独的子表达式可以包含范围和/或列表。例如,可以用“MON-FRI”,“MON,WED,FRI”,甚至“MON-WED,SAT”代替先前(以“WED”)为例的星期几字段。

通配符(“字符”)可以用来表示该字段的“每个”可能的值。因此,上例中“月”字段中的字符只是“每月”。因此,“星期几”字段中的“*”显然意味着“每周的每一天”。

所有的字段都有一组可以指定的有效值。这些值应该相当明显 - 例如秒和分钟的数字0到59,以及数小时的值0到23。月的日期可以是1-31的任何值,但是您需要小心给定的月份有多少天!可以将月份指定为0到11之间的值,或者使用字符串JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV和DEC。可以将星期几指定为1到7之间的值(1 =星期日),或者使用字符串SUN,MON,TUE,WED,THU,FRI和SAT。

“/”字符可用于指定增量值。例如,如果在“分钟”字段中输入“0/15”,则表示“每分钟15分钟,从零开始”。如果在“分钟”字段中使用“3/20”,则意味着“从第三分钟开始的每20分钟一小时” - 或者换句话说,就是在分钟中指定“3,23,43”领域。请注意“ / 35”并不意味着“每35分钟” 的微妙含义,即“每分钟35分钟,从零开始”,换句话说就是指定“0,35”。

'?' 字符被允许用于日期和星期几字段。它用来指定“没有具体的价值”。当你需要在两个字段中的一个字段中指定某些内容时,这是非常有用的,而不是其他的。请参阅下面的示例(和CronTrigger JavaDoc)进行说明。

月份和星期几字段允许使用“L”字符。这个角色对于“最后”来说是短暂的,但是在两个领域中的每一个都有不同的含义。例如,月份字段中的值“L”意味着“月份的最后一天”,即非闰年的2月28日的1月31日。如果单独使用在星期几字段中,则仅表示“7”或“SAT”。但是,如果在星期几字段中使用另一个值,则表示“本月的最后一个xxx日”,例如“6L”或“FRIL”都表示“本月的最后一个星期五”。您还可以指定月份的最后一天的偏移量,例如“L-3”,表示日历月份的倒数第三天。当使用“L”选项时,不要指定列表或值的范围,因为您会得到令人困惑/意外的结果。

“W”用于指定与指定日期最近的星期几(星期一至星期五)。例如,如果您要指定“15W”作为月份日期字段的值,则其含义是:“最近的星期几到本月15日”。

“#”用于指定该月的第n个“XXX”工作日。例如,星期几字段中的“6#3”或“FRI#3”的值表示“月的第三个星期五”。

示例Cron表达式

CronTrigger示例1 -创建一个触发器的表达式,每5分钟查阅【木子道】公众号
“0 0/5 * * *?”

CronTrigger示例2 - 一个表达式,用于创建在分钟后10秒(即上午10:00:10,上午10:05:10等)每5分钟触发一次的触发器。
“10 0/5 * * *?”

CronTrigger示例3 - 一个表达式,用于创建在每周三和周五的10:30,11:30,12:30和13:30发生的触发器。
“0 30 10-13?* WED,FRI“

CronTrigger示例4 - 一个表达式,用于创建一个触发器,在每个月的第5天和第20天的上午8点到上午10点之间每隔半小时触发一次。请注意,触发器不会在上午10点,仅在8点,8点,9点和9点30分
“0 0/30 8-9 5,20 *?”

请注意,一些调度要求过于复杂,无法用一个触发器来表示 - 例如“上午9点至上午10点之间每5分钟一次,下午1点至10点之间每20分钟一次”。在这种情况下解决方案是简单地创建两个触发器,并注册他们两个运行相同的工作。

JobStore
JobStore负责跟踪所有给调度器的“工作数据”:作业,触发器,日历等。为您的Quartz调度器实例选择合适的JobStore是非常重要的一步。幸运的是,一旦你了解它们之间的差异,选择应该是一个非常简单的选择。您可以在您提供给SchedulerFactory的属性文件(或对象)中声明您的调度程序应使用哪个JobStore(以及它的配置设置),以便生成调度程序实例。

切勿在代码中直接使用JobStore实例。出于某种原因,许多人试图这样做。JobStore用于石英本身的幕后使用。你必须告诉Quartz(通过配置)使用哪个JobStore,但是你只能在你的代码中使用Scheduler接口。

RAMJobStore
RAMJobStore是最简单的JobStore,它也是性能最高的(CPU时间)。RAMJobStore以明显的方式得到它的名字:它将所有的数据保存在RAM中。这就是为什么它闪电般快速,也是为什么配置如此简单。缺点是,当你的应用程序结束(或崩溃)时,所有的调度信息都将丢失 - 这意味着RAMJobStore无法遵守作业和触发器的“非易失性”设置。对于某些应用程序来说,这是可以接受的 - 甚至是所需的行为,但是对于其他应用程序来说,这可能是灾难性的。

JDBCJobStore
JDBCJobStore也被恰当地命名 - 它通过JDBC将其所有数据保存在数据库中。因此,配置比RAMJobStore要复杂一些,而且速度也不是那么快。但是,性能退化不是非常糟糕,特别是如果您使用主键上的索引构建数据库表时。在相当现代的机器上有一个体面的局域网(在调度器和数据库之间),检索和更新触发器的时间通常小于10毫秒。

JDBCJobStore几乎可以与任何数据库一起使用,它已经被广泛地用于Oracle,PostgreSQL,MySQL,MS SQLServer,HSQLDB和DB2。要使用JDBCJobStore,必须先为Quartz创建一组数据库表来使用。您可以在Quartz发行版的“docs / dbTables”目录中找到创建表的SQL脚本。如果您的数据库类型没有脚本,只需查看其中一个脚本,然后以任何必要的方式修改它。有一点需要注意的是,在这些脚本中,所有的表都以前缀“QRTZ_”开头(例如表“QRTZ_TRIGGERS”和“QRTZ_JOB_DETAIL”)。这个前缀实际上可以是任何你想要的,只要你通知JDBCJobStore什么前缀(在你的Quartz属性中)。使用不同的前缀对于创建多个表集合,对于多个调度器实例,

一旦创建了表,在配置和启动JDBCJobStore之前,还有一个重要的决定。您需要确定您的应用程序需要什么类型的事务。如果您不需要将调度命令(如添加和删除触发器)与其他事务绑定,那么您可以让Quartz使用JobStoreTX作为JobStore(这是最常见的选择)来管理事务。

如果您需要Quartz与其他事务(即在J2EE应用程序服务器中)一起工作,那么您应该使用JobStoreCMT--在这种情况下,Quartz将让应用程序服务器容器管理事务。

最后一块难题是设置一个JDBCJobStore可以连接到数据库的DataSource。DataSources是使用几种不同的方法之一在你的Quartz属性中定义的。一种方法是让Quartz创建和管理DataSource本身 - 通过提供数据库的所有连接信息。另一种方法是让Quartz使用由Quartz运行的应用程序服务器管理的DataSource - 通过向JDBCJobStore提供DataSource的JNDI名称。

TerracottaJobStore
TerracottaJobStore在不使用数据库的情况下提供了扩展性和健壮性的手段。这意味着您的数据库可以从Quartz中免费下载,并且可以将其所有资源保存到您的应用程序的其余部分。

TerracottaJobStore可以运行群集或非群集,并且在任何情况下为应用程序重新启动之间的持续工作数据提供存储介质,因为数据存储在Terracotta服务器中。它的性能比通过JDBCJobStore使用数据库要好得多(大约好一个数量级),但是比RAMJobStore慢得多。

3.Quartz 的配置与使用

//配置SchedulerFactory
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setDataSource(dataSource);
        //quartz参数
        Properties prop = new Properties();
        prop.put("org.quartz.scheduler.instanceName", "AstaliScheduler");
        prop.put("org.quartz.scheduler.instanceId", "AUTO");
        //线程池配置
        prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
        prop.put("org.quartz.threadPool.threadCount", "20");
        prop.put("org.quartz.threadPool.threadPriority", "5");
        //JobStore配置
       //SQL脚本。http://svn.terracotta.org/svn/quartz/tags/quartz-2.1.3/docs/dbTables/ 
        prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
        //集群配置
        prop.put("org.quartz.jobStore.isClustered", "true"); //开启集群
        prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
        prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
        prop.put("org.quartz.jobStore.misfireThreshold", "12000");
        prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");  //表头
        factory.setQuartzProperties(prop);
        factory.setSchedulerName("AstaliScheduler");
        //延时启动
        factory.setStartupDelay(30);
        factory.setApplicationContextSchedulerContextKey("applicationContextKey");
        //可选,QuartzScheduler 启动时更新己存在的Job,
        //这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了
        factory.setOverwriteExistingJobs(true);
        //设置自动启动,默认为true
        factory.setAutoStartup(true);
        return factory;
    }
}
//构建Job实例   创建任务
//ScheduleJob extents QuartzJobBean,QuartzJobBean implements Job
 JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.class)
                    .withIdentity(getJobKey(scheduleJobEntity.getJobId()))
                    .build();

//构建cron
  CronScheduleBuilder scheduleBuilde = CronScheduleBuilder
                                     .cronSchedule(scheduleJob.getCronExpression())
                                     .withMisfireHandlingInstructionDoNothing();
 //根据cron,构建一个CronTrigger
 CronTrigger trigger = TriggerBuilder.newTrigger()
                          .withIdentity(getTriggerKey(scheduleJob.getJobId()))
                          .withSchedule(scheduleBuilder)
                          .build();
//调度器的任务类与触发器关联
 scheduler.scheduleJob(jobDetail, trigger);
//立即执行任务
  JobDataMap dataMap = new JobDataMap();
  dataMap.put(ScheduleJobEntity.JOB_PARAM_KEY, new 
Gson().toJson(scheduleJob));
 scheduler.triggerJob(getJobKey(scheduleJob.getJobId()), dataMap);
//暂定任务
scheduler.pauseJob(getJobKey(jobId));
  //恢复任务
 scheduler.resumeJob(getJobKey(jobId));
  //删除任务
 scheduler.deleteJob(getJobKey(jobId));
    /**
     * 获取触发器key
     */
    private static TriggerKey getTriggerKey(Long jobId) {
        return TriggerKey.triggerKey(JOB_NAME + jobId);
    }
     /**
     * 获取jobKey
     */
    private static JobKey getJobKey(Long jobId) {
        return JobKey.jobKey(JOB_NAME + jobId);
    }
    /**
     * 获取表达式触发器
     */
    public static CronTrigger getCronTrigger(Scheduler scheduler, Long jobId) {
        try {
            return (CronTrigger) scheduler.getTrigger(getTriggerKey(jobId));
        } catch (SchedulerException e) {
            throw new Exception("getCronTrigger异常,请检查qrtz开头的表,是否有脏数据", e);
        }
    }
//上篇文章对newSingleThreadExecutor有说明
ExecutorService service = Executors.newSingleThreadExecutor();    
//执行定时任务实现Runnable并重写run方法
ScheduleRunnable task = new ScheduleRunnable(scheduleJob.getBeanName(),               
                              scheduleJob.getMethodName(),     
                              scheduleJob.getParams());
//提交任务并有结果
Future<?> future = service.submit(task); 
future.get();

    @Override
    public void run() {
        try {
   //利用反射执行方法
            ReflectionUtils.makeAccessible(method);
            if(StringUtils.isNotBlank(params)){
                method.invoke(target, params);
            }else{
                method.invoke(target);
            }
        }catch (Exception e) {
            throw new Exception("执行定时任务失败", e);
        }
    }
建立一个触发器,每隔上午8点到下午5点,每隔一分钟一次:
  trigger = newTrigger()
    .withIdentity("trigger3", "group1")
    .withSchedule(cronSchedule("0 0/2 8-17 * * ?"))
    .forJob("myJob", "group1")
    .build();
建立一个触发器,每天在上午10:42开枪:
  trigger = newTrigger()
    .withIdentity("trigger3", "group1")
    .withSchedule(dailyAtHourAndMinute(10, 42))
    .forJob(myJobKey)
    .build();

效果图


前端效果图

需要源码的请关注公众号【木子道】

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容