一、介绍
1.1 技术介绍
Elastic-Job是一个批量任务调度框架,解决了分布式系统定时完成任务较为困难的问题,下面对任务调度及该框架进行简要介绍。
1.1.1 分布式任务调度简介
任务调度是指系统为了自动完成特定任务,在约定的特定时刻去执行任务的过程。而分布式任务调度即在分布式系统环境下运行任务调度。
1.1.2 Elastic-Job简介
Elastic-Job是基于quartz 二次开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,实现任务高可用以及分片,解决了quartz框架对于分布式系统支持不够好的问题,具有调度策略丰富,作业分片一致性,支持并行调度等优点。
1.2 项目地址
GitHub主页:
https://github.com/apache/shardingsphere-elasticjob
1.3 程序版本
Elastic-Job版本:2.1.5
二、关键技术
2.1 Elatic-Job整体架构
官方文档中给出了Elastic-Job分布式任务调度框架的整体结构,下面对主要部分进行简要说明。
- App:应用程序,内部包含任务执行业务逻辑和Elastic-Job-Lite组件,其中执行任务需要实现ElasticJob接口完成与Elastic-Job-Lite组件的集成,并进行任务的相关配置。应用程序可启动多个实例,也就出现了多个任务执行实例。
- Elastic-Job-Lite:Elastic-Job-Lite定位为轻量级无中心化解决方案,使用jar包的形式提供分布式任务的协调服务,此组件负责任务的调度,并产生日志及任务调度记录。
- Registry:以Zookeeper作为Elastic-Job的注册中心组件,存储了执行任务的相关信息。同时,Elastic-Job利用该组件进行执行任务实例的选举。
- Console:Elastic-Job提供了运维平台,它通过读取Zookeeper数据展现任务执行状态,或更新Zookeeper数据修改全局配置。通过Elastic-Job-Lite组件产生的数据来查看任务执行历史记录。
2.2 Elatic-Job启动流程
应用程序在启动时,在其内嵌的Elastic-Job-Lite组件会向Zookeeper注册该实例的信息,并触发选举(此时可能已经启动了该应用程序的其他实例),从众多实例中选举出一个Leader,让其执行任务。当到达任务执行时间时,Elastic-Job-Lite组件会调用由应用程序实现的任务业务逻辑,任务执行后会产生任务执行记录。当应用程序的某一个实例宕机时,Zookeeper组件会感知到并重新触发leader选举。
三、环境搭建
Elastic-Job只需要在pom文件中添加依赖即可加入项目,启动时需要先启动Zookeeper作为注册中心
3.1 配置
3.1.1 Zookeeper配置
启动Zookeeper
3.1.2 Elastic-Job配置
在pom文件中引入Elastic-Job的依赖
4.0.0
cn.toj
elasticjobdemo
1.0-SNAPSHOT
jar
UTF-8
UTF-8
1.8
com.dangdang
elastic-job-lite-core
2.1.5
${project.name}
org.apache.maven.plugins
maven-compiler-plugin
1.8
四、Demo开发演示
Demo的主要功能是演示Elastic-Job的定时任务调度及分片功能,模拟多个服务端同时按照规定的时间在控制台输出当前时间,及当一个服务端终止服务时,自动切换的功能。
4.1 Demo概述
4.1.1 整体架构
Demo的整体架构比较简单,主要由两个文件组成,负责任务逻辑的JobConifg
文件及运行任务的JobMain
文件。
4.1.2 任务逻辑
Demo的任务主要在控制台输入当前时间,及当前分片数,分片数由Elatic-Job自动传入的ShardingContext
类型的对象提供
其中最重要的是任务逻辑类实现了SimpleJob
接口,并在类中实现了execute
方法。
package cn.toj.elasticjobdemo.job;
import com.dangdang.ddframe.job.api.ShardingContext;
import com.dangdang.ddframe.job.api.simple.SimpleJob;
import java.time.LocalDateTime;
/**
* @author Carlos
* @description
* @Date 2020/8/13
*/
public class JobConfig implements SimpleJob {
//任务执行代码逻辑
@Override
public void execute(ShardingContext shardingContext) {
System.out.println("当前分片:" + shardingContext.getShardingItem());
System.out.printf("当前时间:%s \n", LocalDateTime.now());
}
}
4.1.3 运行任务
启动类由4部分构成
4.1.3.1 常量
常量设定了zookeeper的地址及该任务在zookeeper中的命名空间,用于区别于其他任务
//zookeeper端口
private static final int ZOOKEEPER_PORT = 2181;
//zookeeper链接字符串 localhost:2181
private static final String ZOOKEEPER_CONNECTION_STRING = "localhost:" + ZOOKEEPER_PORT;
//定时任务命名空间
private static final String JOB_NAMESPACE = "elastic-job-example-java-test";
4.1.3.2 主函数
主函数中确定了整个任务的流程,分为配置注册中心及启动任务
//执行启动任务
public static void main(String[] args) {
//配置注册中心
CoordinatorRegistryCenter registryCenter = setUpRegistryCenter();
//启动任务
startJob(registryCenter);
}
4.1.3.3 配置注册中心
填入zookeeper注册中心的地址及任务的名称,再创建注册中心,最后启动并返回该注册中心。
//zk的配置及创建注册中心
private static CoordinatorRegistryCenter setUpRegistryCenter(){
//zk的配置
ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration(ZOOKEEPER_CONNECTION_STRING, JOB_NAMESPACE);
//创建注册中心
CoordinatorRegistryCenter zookeeperRegistryCenter = new ZookeeperRegistryCenter(zookeeperConfiguration);
zookeeperRegistryCenter.init();
return zookeeperRegistryCenter;
}
4.1.3.4 配置及启动任务
传入上一步启动的注册中心,并将任务逻辑类的全限定类名传入任务调度中,根据Cron表达式(3.2节介绍Cron表达式的写法)代表的周期,任务将规定的周期进行运行,本Demo的周期为从0秒开始,每三秒运行一次。
//任务的配置和启动
private static void startJob(CoordinatorRegistryCenter registryCenter){
//String jobName 任务名称, String cron 调度表达式, int shardingTotalCount 作业分片数量
JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder("job-test", "0/3 * * * * ?", 1).build();
//创建SimpleJobConfiguration
SimpleJobConfiguration simpleJobConfiguration = new SimpleJobConfiguration(jobCoreConfiguration, JobConfig.class.getCanonicalName());
//创建new JobScheduler
new JobScheduler(registryCenter, LiteJobConfiguration.newBuilder(simpleJobConfiguration).overwrite(true).build()).init();
}
4.2 Cron表达式详解
Cron表达式主要用于配置任务调度的周期,可以根据年月日或者星期来进行规定启动时间,如可规定每月末(可为28/29/30/31日)进行启动,由7个子表达式构成,至少由六个子表达式构成;表达式之间由空格构成,每个表达式的意义如下:
1.Seconds (秒)
2.Minutes(分)
3.Hours(小时)
4.Day-of-Month (天)
5.Month(月)
6.Day-of-Week (周)
7.Year(年)可选
以Demo中的表达式为例:0/3 * * * * ?
,其中 0/3
表示从0秒开始,每三秒执行一次;
另一个表达式:3 * * * * ?
,表示每分钟的第三秒执行一次,即/
前的数字为在每个位置的第几,而/
后面的数字为以位置为起点,周期为多少。
4.2.1 Cron表达式各个位置的取值范围
表达式 | 取值范围 |
---|---|
Seconds (秒) | 0~59 |
Minutes(分) | 0~59 |
Hours(小时) | 0~23 |
Day-of-Month(天) | 1~31(有些月份没有31天) |
Month(月) | 0~11* |
Day-of-Week (周) | 1~7 |
.Year(年) | 1970~2099 |
注:表格中的月份可使用英文简写,JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV,DEC
,星期几同样可写成SUN, MON, TUE, WED, THU, FRI, SAT
,注意欧美的每周是从周日开始的,所以”周“的”1“是周日,为避免混淆,建议优先使用英文缩写;年份如不需规定可以省略。
4.2.2 字符使用方法
字符使用较为复杂,请看以下各个字符的介绍
-
*
代表所有可能的值。因此,*
在Month中表示每个月,在Day-of-Month中表示每天,在Hours表示中每小时 -
-
表示指定范围,如在”周“位填写MON-THU
代表每周的周一到周四运行任务。 -
,
表示枚举间隔,如在“月”位填写FEB,APR,MAY
代表在“二月,四月,五月”运行任务。 -
?
比较重要,是为避免"Day-of-Month(天)"和“Day-of-Week (周)”之间产生冲突而写的占位符,如分别填写“21”和“WED”,则会产生某月的21号不是周三的冲突,所以填写这两个其中一个后另一个必须以?
进行填充。如Demo中的0/3 * * * * ?
填写了“天”后“周”置为?
。 -
L
可以用在“Day-of-Month(天)”和“Day-of-Week (周)”中,是单词“last”的缩写,他们分别表示为“月份的最后一天(28,29,30或31)”及“一周的最后一天(周六SAT)”;同时它还可以接在数字后,如在月份中“5L表示该月的倒数第五天(可能是24,25,26,27日)”,而在周中“FRIL”表示本月的最后一个周五。 -
W
是Weekday”的缩写。只能用在“day-of-month(天)”字段。用来描叙最接近指定天的工作日(周一到周五)。例如:在day-of-month字段用“15W”指“最接近这个月第15天的工作日”,即如果这个月第15天是周六,那么触发器将会在这个月第14天即周五触发;如果这个月第15天是周日,那么触发器将会在这个月第 16天即周一触发;如果这个月第15天是周二,那么就在触发器这天触发。注意:这个用法只会在当前月计算值,不会越过当前月。“W”字符仅能在 day-of-month指明一天,不能是一个范围或列表。也可以用“LW”来指定这个月的最后一个工作日,即最后一个星期五。 -
#
只能用在“day-of-week(周)”字段。用来指定这个月的第几个周几。例如:在day-of-week字段用"6#3" or "FRI#3"指这个月第3个周五(6指周五,3指第3个)。如果指定的日期不存在,触发器就不会触发。
4.3 任务调度测试
启动main函数,查看控制台输出,从0秒开始,每3秒运行一次
当前分片:0
当前时间:2020-08-17T15:55:24.082
当前分片:0
当前时间:2020-08-17T15:55:27.009
当前分片:0
当前时间:2020-08-17T15:55:30.010
当前分片:0
当前时间:2020-08-17T15:55:33.010
当前分片:0
当前时间:2020-08-17T15:55:36.009
修改Cron表达式,设定成每分钟的5秒,及20秒运行,表达式为5,20 * * * * ?
,运行结果
当前分片:0
当前时间:2020-08-17T16:01:05.071
当前分片:0
当前时间:2020-08-17T16:01:20.008
当前分片:0
当前时间:2020-08-17T16:02:05.010
当前分片:0
当前时间:2020-08-17T16:02:20.008
当前分片:0
当前时间:2020-08-17T16:03:05.009
当前分片:0
当前时间:2020-08-17T16:03:20.010
4.4 分片测试
JobCoreConfiguration.newBuilder
的三个参数分别为:任务名,Cron表达式,分片数,分片数大于1且小于进程数时,相同名称的任务之间通过锁进行争抢,当一个进程中断时,空闲的进程获得锁从而运行;而分片数大于进程数时,一个进程可分配到多于一个分片。以下为各种情况的测试。
4.4.1 一个进程,3个分片
当前进程获得所有分片
当前分片:0
当前分片:1
当前分片:2
当前时间:2020-08-17T16:08:21.074
当前时间:2020-08-17T16:08:21.074
当前时间:2020-08-17T16:08:21.074
当前分片:0
当前时间:2020-08-17T16:08:24.015
当前分片:1
当前时间:2020-08-17T16:08:24.015
当前分片:2
当前时间:2020-08-17T16:08:24.015
当前分片:0
当前时间:2020-08-17T16:08:27.014
当前分片:1
当前时间:2020-08-17T16:08:27.014
当前分片:2
当前时间:2020-08-17T16:08:27.015
4.4.2 两个进程,3个分片
一个进程获得两个,另一个获得一个
进程一:
当前分片:0
当前时间:2020-08-17T16:09:57.013
当前分片:2
当前时间:2020-08-17T16:09:57.014
当前分片:0
当前时间:2020-08-17T16:10:00.013
当前分片:2
当前时间:2020-08-17T16:10:00.013
当前分片:0
当前时间:2020-08-17T16:10:03.015
当前分片:2
当前时间:2020-08-17T16:10:03.015
进程二:
当前分片:1
当前时间:2020-08-17T16:09:57.008
当前分片:1
当前时间:2020-08-17T16:10:00.010
当前分片:1
当前时间:2020-08-17T16:10:03.009
4.4.3 三个进程,3个分片
每个进程获得一个分片
进程一:
当前分片:2
当前时间:2020-08-17T16:12:18.047
当前分片:2
当前时间:2020-08-17T16:12:21.008
当前分片:2
当前时间:2020-08-17T16:12:24.009
进程二:
当前分片:1
当前时间:2020-08-17T16:12:18.112
当前分片:1
当前时间:2020-08-17T16:12:21.009
当前分片:1
当前时间:2020-08-17T16:12:24.008
进程三:
当前分片:0
当前时间:2020-08-17T16:12:18.073
当前分片:0
当前时间:2020-08-17T16:12:21.016
当前分片:0
当前时间:2020-08-17T16:12:24.012
4.4.4 四个进程,三个分片及突然中断一个进程
发生争抢,运行结果和3.4.3 相同,但是有一个进程没有抢到锁所以不运行,处在等待状态,当一个进程突然中断时,该进程获得锁,从而获得一个分片。
以上模拟了多台服务器运行相同的任务的分片情况。
4.5 Demo下载地址
- GitHub项目地址:
https://github.com/diyzhang/42j121-elasticjobdemo
- 使用Git下载项目的命令:
git clone https://github.com/diyzhang/42j121-elasticjobdemo.git
5. Cron表达式测验
- 每年的父亲节中午十二点运行任务(父亲节为每年6月的第三个星期日)。
- 以下Cron表达式代表的日期:
0 0 17 ? * TUES,THUR,SAT
0 15 10 ? * FRIL 2002-2005