SpringBoot整合分布式任务调度Elastic-Job
Elastic-Job介绍
elastic-job是当当网基于quartz 二次开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,实现任务高可用以及分片。Elastic-Job是一个分布式调度的解决方案,由当当网开源,它由两个相互独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成,使用Elastic-Job可以快速实现分布式任务调度。
ElasticJob 已于 2020 年 5 月 28 日成为 Apache ShardingSphere 的子项目。
官方文档:https://shardingsphere.apache.org/elasticjob/index_zh.html
zk在elastic-job的作用
- Elastic-Job依赖ZooKeeper完成对执行任务信息的存储(如任务名称、任务参与实例、任务执行策略等);
- Elastic-Job依赖ZooKeeper实现选举机制,在任务执行实例数量变化时(如在快速上手中的启动新实例或停止实例),会触发选举机制来决定让哪个实例去执行该任务。
功能特性
分布式调度协调
在分布式环境中,任务能够按指定的调度策略执行,并且能够避免同一任务多实例重复执行。
弹性扩容缩容
当集群中增加某一个实例,它也能够被选举并执行任务;当集群减少一个实例时,它所执行的任务能被转移到别的实例来执行。
丰富的作业类型
支持Simple、DataFlow、Script三种作业类型。
丰富的调度策略
基于成熟的定时任务作业框架Quartz cron表达式执行定时任务。
失效转移
某实例在任务执行失败后,会被转移到其他实例执行。
错过执行作业重触发
若因某种原因导致作业错过执行,自动记录错过执行的作业,并在上次作业完成后自动触发。
支持并行调度
支持任务分片,任务分片是指将一个任务分为多个小任务项在多个实例同时执行。
作业分片一致性
当任务被分片后,保证同一分片在分布式环境中仅一个执行实例。
支持作业生命周期操作
可以动态对任务进行开启及停止操作。
Spring 整合以及命名空间支持
对Spring支持良好的整合方式,支持spring自定义命名空间,支持占位符。
运维平台
提供运维界面,可以管理作业和注册中心。
SpringBoot集成Elastic-Job
非场景启动器starter
引入相关依赖
<!-- elastic job-->
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-spring</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-core</artifactId>
<version>2.1.5</version>
</dependency>
application.yml配置elasticjob
server:
port: 9999
# 分布式任务调度自定义配置参数
elasticjob:
# 注册中心
reg-center:
server-list: 192.168.198.155:2182,192.168.198.155:2183,192.168.198.155:2184 #zookeeper地址
namespace: elastic-job-springboot
# 作业配置
jobs:
list:
- clazz: com.harvey.demo.job.SpringBootJob
cron: 0/20 * * * * ?
shardingTotalCount: 2
shardingItemParam: 0=text,1=image
jobParam: name=test
jobName: SpringBootJob # 本实例中没有特殊含义
- clazz: com.harvey.demo.job.SpringRunnerJob
cron: 0/20 * * * * ?
shardingTotalCount: 3
shardingItemParam: 0=text,1=image,2=video
jobParam: name=test
jobName: SpringRunnerJobs # 本实例中没有特殊含义
注册中心、job实例配置
1)job定义
第一个job:
@Component
public class SpringBootJob implements SimpleJob {
@Override
public void execute(ShardingContext shardingContext) {
System.out.println(this.getClass() + ":分片总数是: [{" + shardingContext.getShardingTotalCount() + "}]; 当前分片项是: [{" + shardingContext.getShardingItem() + "}]");
//分片参数,(0=text,1=image,2=video,参数就是text、image...)
String shardingParameter = shardingContext.getShardingParameter();
//任务参数, 配置是name=test就是name=test
String jobParameter = shardingContext.getJobParameter();
System.out.println("current sharding item " + shardingContext.getShardingItem() + ", shardingParameter = " + shardingParameter + ", jobParameter:" + jobParameter);
}
}
第二个job:
@Component
public class SpringRunnerJob implements SimpleJob {
@Override
public void execute(ShardingContext shardingContext) {
System.out.println(this.getClass() + ":分片总数是: [{" + shardingContext.getShardingTotalCount() + "}]; 当前分片项是: [{" + shardingContext.getShardingItem() + "}]");
//分片参数,(0=text,1=image,2=video,参数就是text、image...)
String shardingParameter = shardingContext.getShardingParameter();
//任务参数
String jobParameter = shardingContext.getJobParameter();
System.out.println("current sharding item " + shardingContext.getShardingItem() + ", shardingParameter = " + shardingParameter + ", jobParameter:" + jobParameter);
}
}
2)注册中心
@Configuration
public class ElasticJobRegistryCenterConfig {
@Value("${elasticjob.reg-center.server-list}")
private String zookeeperAddressList;
@Value("${elasticjob.reg-center.namespace}")
private String namespace;
/****
* 注册中心
* @return
*/
@Bean(initMethod = "init")
public CoordinatorRegistryCenter zkCenter() {
// 1 zk的配置
// 参数一:zk的地址,如果是集群,每个地址用逗号隔开
ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration(zookeeperAddressList, namespace);
// 创建协调注册中心
// 2 CoordinatorRegistryCenter接口,elastic-job提供了一个实现ZookeeperRegistryCenter
CoordinatorRegistryCenter zookeeperRegistryCenter = new ZookeeperRegistryCenter(zookeeperConfiguration);
// 3 注册中心初始化
//zookeeperRegistryCenter.init();
return zookeeperRegistryCenter;
}
}
3)job配置
job基础信息:
/**
* job基础信息
*/
public class JobScheduler {
/**
* 继承ElasticJob的类的全路径名称
*/
private String clazz;
/**
* cron表达式
*/
private String cron;
/**
* 分片数
*/
private int shardingTotalCount;
/**
* 分片参数
*/
private String shardingItemParam;
/**
* 任务参数
*/
private String jobParam;
/**
* job名称
*/
private String jobName;
public String getClazz() {
return clazz;
}
public void setClazz(String clazz) {
this.clazz = clazz;
}
public String getCron() {
return cron;
}
public void setCron(String cron) {
this.cron = cron;
}
public int getShardingTotalCount() {
return shardingTotalCount;
}
public void setShardingTotalCount(int shardingTotalCount) {
this.shardingTotalCount = shardingTotalCount;
}
public String getShardingItemParam() {
return shardingItemParam;
}
public void setShardingItemParam(String shardingItemParam) {
this.shardingItemParam = shardingItemParam;
}
public String getJobParam() {
return jobParam;
}
public void setJobParam(String jobParam) {
this.jobParam = jobParam;
}
public String getJobName() {
return jobName;
}
public void setJobName(String jobName) {
this.jobName = jobName;
}
}
SpringContextUtil工具类
/**
* 获取springBean对象
*/
@Component
public class SpringContextUtil implements ApplicationContextAware {
// spring上下文
public static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.applicationContext = applicationContext;
}
public ApplicationContext getApplicationContext() {
return applicationContext;
}
public <T> T getBean(Class<T> tClass) {
return applicationContext.getBean(tClass);
}
public <T> Map<String, T> getBeansOfType(Class<T> tClass) {
return applicationContext.getBeansOfType(tClass);
}
/**
* 获取接口下的所有实现类
*
* @param tClass
* @param <T>
*/
public <T> List<T> getBeansOfTypeList(Class<T> tClass) {
Map<String, T> beanOfType = getBeansOfType(tClass);
List<T> list = new ArrayList<>();
beanOfType.forEach((k, v) -> {
list.add(v);
});
return list;
}
}
job配置并启动
@Component
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "elasticjob.jobs")
public class ElasticJobConfig implements InitializingBean {
/**
* 任务列表job
*/
private List<JobScheduler> list;
public void setList(List<JobScheduler> list) {
this.list = list;
}
@Autowired
private CoordinatorRegistryCenter zkCenter;
@Autowired
private SpringContextUtil springContextUtil;
@Override
public void afterPropertiesSet() throws Exception {
//这里可以替换为从数据库查询,将JobScheduler的信息配置到表里
Map<String, JobScheduler> jobSchedulerMap = new HashMap(list.size());
for (JobScheduler scheduler : list) {
jobSchedulerMap.put(scheduler.getClazz(), scheduler);
}
//收集所有实现SimpleJob接口的job实例
Map<String, SimpleJob> simpleJobMap = springContextUtil.getApplicationContext().getBeansOfType(SimpleJob.class);
Collection<SimpleJob> simpleJobs = simpleJobMap.values();
for (SimpleJob simpleJob : simpleJobs) {
//根据job实例的全类名获取JobScheduler信息
JobScheduler jobScheduler = jobSchedulerMap.get(simpleJob.getClass().getName());
if (jobScheduler == null) {
throw new RuntimeException("请配置[" + simpleJob.getClass().getName() + "]的JobScheduler");
}
System.out.println("job scheduler init :" + jobScheduler.getClazz());
SpringJobScheduler springJobScheduler = new SpringJobScheduler(simpleJob, zkCenter,
liteJobConfiguration(simpleJob.getClass(), jobScheduler.getCron(), jobScheduler.getShardingTotalCount(),
jobScheduler.getShardingItemParam(), jobScheduler.getJobParam(), jobScheduler.getJobName()));
//启动任务
springJobScheduler.init();
}
}
private LiteJobConfiguration liteJobConfiguration(final Class<? extends ElasticJob> jobClass,
final String cron,
final int shardingTotalCount,
final String shardingItemParam,
final String jobParam,
final String jobName
) {
// 1 job核心配置
JobCoreConfiguration.Builder builder = JobCoreConfiguration.newBuilder(
// job的名字
jobName,
// cron表达式
cron,
// 分片总数
shardingTotalCount
);
// 分片参数不是空,设置分片参数
if (StringUtils.isNotBlank(shardingItemParam)) {
builder.shardingItemParameters(shardingItemParam);
}
// 任务参数不是空,设置任务参数
if(StringUtils.isNotBlank(jobParam)){
builder.jobParameter(jobParam);
}
JobCoreConfiguration jobCoreConfiguration = builder.build();
// 2 job类型配置
// 参数1: job核心配置JobCoreConfiguration
// 参数2 job的全类名
JobTypeConfiguration jobTypeConfiguration = new SimpleJobConfiguration(jobCoreConfiguration, jobClass.getCanonicalName());
// 3 job根配置(LiteJobConfiguration)
LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobTypeConfiguration)
.overwrite(true)
.build();
return liteJobConfiguration;
}
}
启动项目,日志打印:
采用场景启动器starter
引入相关依赖
<!-- 引入版本的时候要充分考虑自己的Zookeeper Server版本, 建议与其保持一致 -->
<!-- 例如 版本使用的是 zk 3.6.x, Zookeeper Server 也是3.6.x, 如果使用3.4.x则会报错 -->
<dependency>
<groupId>org.apache.shardingsphere.elasticjob</groupId>
<artifactId>elasticjob-lite-spring-boot-starter</artifactId>
<version>3.0.1</version>
</dependency>
application.yml中配置注册中心和作业调度
server:
port: 9999
#elasticjob配置
elasticjob:
# 注册中心配置
reg-center:
# 连接 ZooKeeper 服务器的列表, 包括 IP 地址和端口号,多个地址用逗号分隔
server-lists: 127.0.0.1:2188
# ZooKeeper 的命名空间
namespace: elastic-job-spring
# 等待重试的间隔时间的初始毫秒数
base-sleep-time-milliseconds: 1000
# 等待重试的间隔时间的最大毫秒数
maxSleepTimeMilliseconds: 3000
# 最大重试次数
maxRetries: 3
# 会话超时毫秒数
sessionTimeoutMilliseconds: 60000
# 连接超时毫秒数
connectionTimeoutMilliseconds: 15000
# 作业配置, 更多的配置参考官网
jobs:
springJob: # job名
elasticJobClass: com.harvey.demo.job.SpringBootJob
cron: 0/5 * * * * ?
shardingTotalCount: 2
shardingItemParameters: 0=Beijing,1=Shanghai
springTestJob:
elasticJobClass: com.harvey.demo.job.SpringRunnerJob
cron: 0/10 * * * * ?
shardingTotalCount: 3
shardingItemParameters: 0=Beijing,1=Shanghai,2=Guangzhou
job实例
第一个job:
@Component
public class SpringBootJob implements SimpleJob {
@Override
public void execute(ShardingContext shardingContext) {
System.out.println(this.getClass() + ":分片总数是: [{" + shardingContext.getShardingTotalCount() + "}]; 当前分片项是: [{" + shardingContext.getShardingItem() + "}]");
//分片参数,(0=text,1=image,2=video,参数就是text、image...)
String shardingParameter = shardingContext.getShardingParameter();
//任务参数, 配置是name=test就是name=test
String jobParameter = shardingContext.getJobParameter();
System.out.println("current sharding item " + shardingContext.getShardingItem() + ", shardingParameter = " + shardingParameter + ", jobParameter:" + jobParameter);
}
}
第二个job:
@Component
public class SpringRunnerJob implements SimpleJob {
@Override
public void execute(ShardingContext shardingContext) {
System.out.println(this.getClass() + ":分片总数是: [{" + shardingContext.getShardingTotalCount() + "}]; 当前分片项是: [{" + shardingContext.getShardingItem() + "}]");
//分片参数,(0=text,1=image,2=video,参数就是text、image...)
String shardingParameter = shardingContext.getShardingParameter();
//任务参数
String jobParameter = shardingContext.getJobParameter();
System.out.println("current sharding item " + shardingContext.getShardingItem() + ", shardingParameter = " + shardingParameter + ", jobParameter:" + jobParameter);
}
}
启动项目,可以看到相关的日志。
elastic-job控制台 v2.1.5
适用于前面讲的 【非场景启动器starter】形式开发的elastic-job。本人尝试将基于场景启动器starter开发的elastic-job注册在这个控制台,在【作业操作-作业维度】的列表中报错。
下载elastic-job-console
下载地址:https://gitee.com/kancy666/elastic-job-console/releases/2.1.5
解压后启动
(本人是在windows启动)
访问控制台
http://127.0.0.1:8899/# ,默认的账号密码是root/root
控制台操作说明
配置Zookeeper信息
监控平台配置zookeeper的信息是关键,只有连通了zookeeper才能对定时任务进行操作。
1)设置界面语言
点击右上角【选择语言】,下拉出现【中文】进行切换。
2)左边菜单点击【全局配置】选中【注册中心配置】,然后出现一个已配置列表。
在列表的下方有个【添加】按钮,点击添加按钮进行添加
添加的字段说明:
- 注册中心名称:自定义,用于当前列表显示,便于区分
- 注册中心地址:zookeeper的地址,需要连接哪个就填写哪个 【IP:端口】
- 命名空间:任务创建ZookeeperRegistryCenter的时候填写namespace,要对应上,才能看到对应下的任务
- 登录凭证:可不填,默认zookeeper不需要填写,除非设置了zookeeper相关信息
填写完之后,点击右下角【提交】按钮,即完成。
列表中出现新增的注册配置,然后点击该配置最后的操作项【连接】,则连接上zookeeper。
作业操作
1)作业维度
查看当前挂在zookeeper的命名空间下的所有任务,提供删除,编辑,触发,失效等一系列功能。
2)修改作业
3)服务器维度
查看当前连着zookeeper的服务器,提供删除,失效,终止等一些列功能。
点击【详情】可以看到该服务器下的正在运行的定时任务。
ElasticJob-UI监控平台
适用于【场景启动器starter】方式开发的elastic-job。
下载ElasticJob-UI
https://shardingsphere.apache.org/elasticjob/current/cn/downloads/
解压后在bin目录双击启动
(本人是在windows环境)
访问控制台
启动后游览器访问(默认端口是8088):http://127.0.0.1:8088/#/login 用户名/密码 root/root
登录成功后,链接上注册中心,链接成功后便可以进行任务的管理。