elastic-job转成xxl-job
1、分布式任务调度
1.1、什么是任务调度
我们可以先思考一下下面业务场景的解决方案:
- 某电商系统需要在每天上午10点,下午3点,晚上8点发放一批优惠券
- 某银行系统需要在信用卡到期还款日的前三天进行短信提醒
- 某财务系统需要在每天凌晨0:10结算前一天的财务数据,统计汇总
- 12306会根据车次的不同,而设置某几个时间点进行分批放票
- 某网站为了实现天气实时展示,每隔5分钟就去天气服务器获取最新的实时天气信息
以上场景就是任务调度所需要解决的问题。
任务调度是指系统为了自动完成特定任务,在约定的特定时刻去执行任务的过程,有了任务调度就可解放更多的人力有系统自动去执行任务。
约定就是我们提前设置好的调度策略。
任务调度如何实现?
多线程的方式实现
//我们可以开启一个线程,每sleep一段时间,就去检查是否已到预期执行时间。
//以下代码简单实现了任务调度的功能
public static void main(String[] args){
//任务执行间隔时间
final long timeInterval = 1000;
Runnable runnable = new Runnable(){
public void run(){
while(true){
//TODO:something
try{
Thread.sleep(timeInterval);
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
};
Thread thread = new Thread(runnable);
thread.start();
}
上面的代码实现了按一定的间隔时间执行任务调度的功能。
jdk也为我们提供了相关支持,如Timer、ScheduledExecutor,下边我们了解一下。
Timer方式实现:
public static void main(String[] args){
Timer timer = new Timer();
timer.schedule(new TimerTask(){
@Override
public void run(){
//TODO:something
}
}, 1000, 2000); //1秒后开始调度,每2秒执行一次
}
Timer的优点在于简单易用,每个Timer对应一个线程,因此可以同时启动多个Timer并行执行多个任务,同一个Timer中的任务是串行执行。
ScheduledExcutor方式实现:
public static void main(String[] args){
//定义线程池
ScheduledExctorService service = Excutors.newScheduledThreadPool(10);
service.scheduleAtFixedRate(
new Runnable(){
@Override
public void run(){
//TODO:something
System.out.println("todo something");
}
}, 1,
2, TimeUnit.SECONDS);
}
java5推出了基于线程池设计的ScheduledExctor,其设计思想是,每一个被调度的任务都会有线程池中的一个线程去执行,因此任务是并发执行的,相互之间不会收到干扰。
Timer和ScheduledExctor都仅能提供基于开始时间与重复间隔的任务调度,不能胜任更加复杂的任务调度需求。比如,设置每月第一天凌晨1点钟执行任务,复杂调度任务的管理,任务间传递数据等等。
Quartz是一个功能强大的任务调度框架,他可以满足更多更复杂的调度需求,Quartz设计的核心类包括Scheduler,Job,以及Trigger。其中,Job负责定义需要执行的任务,Trigger负责设置调度策略,Scheduler将二者组装在一起,并触发任务开始执行。Quartz支持简单的按时间间隔调度,还支持按日历调度方式,通过设置CronTrigger表达式(包括:秒、分、时、日、月、周、年)进行任务调度。
第三方Quartz方式实现:
public static void main(String[] args) throws SchedulerException {
//创建一个scheduler
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
//创建JobDetail
JobBuilder jobDetailBuilder = JobBuilder.newJob(MyJob.class);
jobDetailBuilder.withIdentity("jobName","jobGroupName");
JObDetail jobDetail = jobDetailBuilder.build();
//创建触发的CronTrigger 支持按日历调度
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("triggerName","triggerGroupName")
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?"))
.build();
//创建触发的SimpleTrigger 简单的间隔调度
/*SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("triggerName","triggerGroupName")
.startNow()
.withSchedule(SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInSeconds(2)
.repeatForever())
.build();*/
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start();
}
public class MyJob implement Job{
@Override
public void execute(JobExecutionContext jobExecutionContext){
System.out.println("todo something");
}
}
通过上面的内容我们学习了什么事任务调度,任务调度所解决的问题,以及任务调度的多种实现方式
1.2、Cron表达式
1.3、什么是分布式任务调度
**什么是分布式?**是一种系统架构
当前软件的架构正在逐步转变为分布式架构,将单体架构分为若干服务,服务之间通过网络交互来完成用户的业务处理,如下图,电商系统为分布式架构,由订单服务、商品服务、用户服务等组成:
分布式系统具体如下基本特点:
1、分布性:每个部分都可以独立部署,服务之间交互通过网络进行通信,比如:订单服务、商品服务。
2、伸缩性:每个部分都可以集群方式部署,并可针对部分节点进行硬件及软件扩容,具有一定的伸缩能力。
3、高可用:每个部分都可以集群部署,保证了高可用。
**什么是分布式调度?**指的是具体的技术解决方案
通常任务调度的程序是集成在应用中的,比如:优惠券服务中包括了定时发放优惠券的调度程序,结算服务中包括了定期生成报表的任务调度程序,由于采用分布式架构,一个服务往往会部署多个冗余实例来运行我们的业务,在这种分布式系统环境下运行任务调度,我们称之为分布式任务调度。
分布式调度要实现的目标:
不管是任务调度程序集成在应用程序中,还是单独构建的任务调度系统,如果采用分布式调度任务的方式,就相当于将任务调度程序分布式构建,这样就可以具有分布式系统的特点,并且提高任务的调度处理能力。
1、并行任务调度
并行任务调度实现要靠多线程,如果有大量任务需要调度,此时光靠多线程就会有瓶颈了,因为一台计算机CPU的处理能力是有限的。
如果将任务调度程序分布式部署,每个节点还可以部署为集群,这样就可以让多台计算机共同去完成任务调度,我们可以将任务分割为若干个分片,由不同的实例并行执行,来提高任务调度的处理效率。
2、高可用
若某一个实例宕机,不影响其他实例来执行任务。
3、弹性扩容
当集群中增加实例就可以提高并行执行任务的处理效率
4、任务管理与监测
对系统中存在的所有定时任务进行统一的管理及监测。让开发人员及运维人员能够时刻了解任务执行情况,从而做出快速的应急处理响应。
5、避免任务重复执行
当任务调度以集群的方式部署,同一个任务调度可能会执行多次,比如在上面提到的电商系统到点发优惠券的例子,就会发放多次优惠券,对公司造成很多损失,所以我们需要控制相同的任务在多个运行实例上只执行一次,考虑采用下边的方法:
- 分布式锁:多个实例在任务执行前首先需要获取锁,如果获取失败那么就证明有其他服务已经在运行了,如果获取成功那么就证明没有服务在运行定时任务,就可以执行。
分布式锁实现方式可采用数据库、redis、Zookeeper等。。。
- Zookeeper选举:利用zookeeper对Leader实例执行定时任务,有其他业务已经使用了ZK,那么定时任务的时候判断自己是否为Leader,如果不是则不执行,如果是则执行业务逻辑,也可以达到目的。
1.4、Elastic-Job介绍
针对分布式任务调度的需求,市场上出现了很多的产品:
1)Elastic-job:当当网基于quartz二次开发的弹性分布式任务调度系统,功能丰富强大,采用zk实现分布式协调,实现任务高可用以及分片。
2)Saturn:唯品会开源的一个分布式任务调度平台,可以全域同一配置,统一监控,任务高可用以及分片并发处理。它是在elastic-job基础之上改良出来的。
3)xxl-job:大众点评的分布式任务调度平台,是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源码并接入多家公司线上产品想,开箱即用。
4)TBSchedule:淘宝的一款非常优秀的高性能分布式调度框架,目前被应用于阿里、京东、支付宝、国美等很多互联网企业的流程调度系统中。
ELastic-job是一个分布式调度的解决方案,由当当网开源,它由两个项目独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成,使用Elastic-JOb可以快速实现分布式任务调度。
Elastic-Job的GitHub地址:https://github.com/elasticjob
功能列表:
- 分布式调度协调
- 在分布式环境中,任务能够按照指定的调度策略执行,并且能够避免同一任务多实例重复执行。
- 丰富的调度策略
- 基于成熟的定时任务作业框架Quartz cron表达式执行定时任务。
- 弹性扩容缩容
- 当集群中增加某一个实例,它也应当能够被选举并执行任务;当集群减少一个实例时,它所能执行的任务能够被转移到别的实例来执行。
- 失效转移
- 某实例在任务执行失败后,会被转移到其他实例执行。
- 错过执行作业重触发
- 若因某种原因导致作业错过执行,自动记录错过执行的作业,并在上次作业完成后自动触发。
- 支持并行调度
- 支持任务分片,任务分片是指将一个任务分为多个小任务项在多个实例同时执行。
- 作业分片一致性
- 当任务被分片后,保证同一分片在分布式环境中仅一个执行实例。
- 支持作业生命周期操作
- 可以动态对任务进行开启及停止操作。
- 丰富的作业类型
- 支持Simple、DataFlow、Script三种作业类型。
2、Elastic-Job学习
2.1、环境搭建
2.1.1、Zookeeper安装&运行
ZK下载地址:https://archive.apache.org/dist/zookeeper
下载.tar.gz之后,解压缩,然后把conf中的zoo_simple.cfg改成zoo.cfg,打开bin目录,使用终端启动sh zkServer.sh
2.2.2、创建maven工程,引入maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>elastic-job-quickstart</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.dangdang</groupId>
<artifactId>elastic-job-lie-core</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
</dependency>
</dependencies>
<build>
<finalName>${project.name}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.2、代码实现
2.2.1、编写定时任务类
此任务在每次执行时获取一定数目的文件,进行备份处理,由File实体类的backUp属性来标识该文件是否已备份。
//具体的执行流程
//1、定时任务类需要实现SimpleJob等接口
//2、实现execute()任务执行的处理方法,在其中写定时任务逻辑。传入的ShardingContext参数可以得出来分片信息等
//3、先获取为备份的文件,然后进行备份文件,并给出提示信息
/**
* 文件实体类
* @author linxiao.cai
* @company td
* @create 2023-12-31 14:01
*/
@Data
public class FileCustom implements Serializable {
/**
* 标识
*/
private String id;
/**
* 文件名
*/
private String name;
/**
* 文件类型
* text,image,radio,vedio
*/
private String type;
/**
* 文件内容
*/
private String content;
/**
* 是否已经备份
*/
private Boolean backedUp = false;
public FileCustom(String id, String name, String type, String content) {
this.id = id;
this.name = name;
this.type = type;
this.content = content;
}
}
任务实例类
/**
* 文件备份
* @author linxiao.cai
* @company td
* @create 2023-12-31 14:14
*/
public class FileBackedUpJob implements SimpleJob {
//每次任务要执行的任务数量
private final int FETCH_SIZE = 1;
//文件列表(模拟)
public static List<FileCustom> files = new ArrayList<>();
@Override
public void execute(ShardingContext shardingContext) {
//作业分片信息
int shardingItem = shardingContext.getShardingTotalCount();
System.out.println(String.format("作业分片:%d",shardingItem));
//1、获取未备份的文件
List<FileCustom> fileCustomList = fetchUnBackUpFiles(FETCH_SIZE);
//2、进行文件备份
backupFiles(fileCustomList);
}
//获取为备份的文件
public List<FileCustom> fetchUnBackUpFiles(int count){
//接收为备份的文件
List<FileCustom> fileCustomList = new ArrayList<>();
//已经获取的文件数量
int num = 0;
//开始获取
for(FileCustom fileCustom : files){
if(num >= count){
break;
}
if(!fileCustom.getBackedUp()){
fileCustomList.add(fileCustom);
num ++;
}
}
//ManagementFactory.getRuntimeMxBean()获取当前JVM进程的PID
System.out.println(String.format("%sTime:%s,已获取%d文件",
ManagementFactory.getRuntimeMXBean().getName(),new SimpleDateFormat("hh:mm:ss").format(new Date()),num));
return fileCustomList;
}
/**
* 备份文件
* @param files
*/
public void backupFiles(List<FileCustom> files){
for(FileCustom file : files){
file.setBackedUp(true);
System.out.println(String.format("已备份文件:%s 文件类型:%s",file.getName(),file.getType()));
}
}
}
启动类
public class Main {
//zk端口
private static final int ZOOKEEPER_PORT = 2181;
//zk链接字符串 localhost:2181
private static final String ZOOKEEPER_CONNECTION_STRING = "localhost:" + ZOOKEEPER_PORT;
//定时任务的命名空间
private static final String JOB_NAMESPACE = "elastic-job-example-java";
//启动任务
public static void main(String[] args) {
//生成测试文件
generateTestFiles();
//配置zk
CoordinatorRegistryCenter coordinatorRegistryCenter = setUpRegistryCenter();
//启动任务
startJob(coordinatorRegistryCenter);
}
/**
* 注册中心配置
* @return
*/
private static CoordinatorRegistryCenter setUpRegistryCenter() {
//注册中心配置
ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration(ZOOKEEPER_CONNECTION_STRING, JOB_NAMESPACE);
//减少zk的超时时间
zookeeperConfiguration.setSessionTimeoutMilliseconds(600000);
//创建注册中心
CoordinatorRegistryCenter zookeeperRegistryCenter = new ZookeeperRegistryCenter(zookeeperConfiguration);
zookeeperRegistryCenter.init();
return zookeeperRegistryCenter;
}
/**
* 启动任务
* @param zookeeperRegistryCenter
*/
public static void startJob(CoordinatorRegistryCenter zookeeperRegistryCenter){
//创建JobCoreConfiguration
//jobName 任务名称 corn:作业调度表达式 shardingTotalCount:分片数量
JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder("file-job", "0/2 * * * * ?", 3).build();
//创建SimpleJObConfiguration 通过这个对象,找到写的任务类
SimpleJobConfiguration simpleJobConfiguration = new SimpleJobConfiguration(jobCoreConfiguration, FileBackedUpJob.class.getCanonicalName());
//启动任务 参数为注册中心 overwrite为true代表:当作业数据更改的时候,zk里面的数据也会被更改
new JobScheduler(zookeeperRegistryCenter, LiteJobConfiguration.newBuilder(simpleJobConfiguration).overwrite(true).build()).init();
}
/**
* 制造一些数据
*/
private static void generateTestFiles() {
for (int i = 1; i < 11; i++){
FileBackedUpJob.files.add(new FileCustom(String.valueOf(i+10),"文件"+(i+10),"text","content"+(i+10)));
FileBackedUpJob.files.add(new FileCustom(String.valueOf(i+10),"文件"+(i+10),"image","content"+(i+10)));
FileBackedUpJob.files.add(new FileCustom(String.valueOf(i+10),"文件"+(i+10),"radio","content"+(i+10)));
FileBackedUpJob.files.add(new FileCustom(String.valueOf(i+10),"文件"+(i+10),"vedio","content"+(i+10)));
}
System.out.println("测试数据制造完成!");
}
}
2.2.2、测试
1)启动main方法查看控制台,定时任务每3秒批量执行一次,符合基础预期
2)测试窗口1不关闭,再次运行main方法观察控制台日志(窗口2)
会出现以下两种情况:
窗口1继续执行任务,窗口2不执行任务
窗口2接替窗口1执行任务,窗口1停止执行任务
3)窗口1、窗口2同时运行的情况下,停止正在执行任务的窗口
未停止的窗口开始执行任务
分片测试:
当前作业没有被分片,所以多个实例共同执行时只有一个实例在执行,如果我们将做作业分片执行,作业将被拆分为多个独立的任务项,然后又分布式的应用实例分别执行某一个或几个分片项。
将分片数量改为3个,然后启动3个窗口,发现每个JobMain窗口分别执行一片作业。
总结:通过上面简单的测试,就可以看出Elastic-Job帮我们解决了分布式调度的以下三个问题:
1)高可用,当同时启动两个实例的时候,一个实例挂掉,另外一个可以保证任务继续进行
2)分布式,可以部署多个实例同时启动
3)弹性扩容,当集群中增加某一个实例,它也应当能够被选举并执行任务,如果作业分片将参与执行某一个分片作业
2.3、Elastic-Job工作原理
2.3.1、Elastic-Job整体架构1
App:应用程序,内部包含任务执行业务逻辑和Elastic-Job-Lite组件,其中执行任务需要实现ElasticJob接口完成与Elastic-Job-Lite组件的集成,并进行任务的相关配置。应用程序可启动多个实例,也就出现了多个任务执行实例。
Elastic-Job-Lite:Elastic-Job-Lite定位为轻量级无中心化解决方案,使用jar包的形式提供分布式任务的协调服务,此组件负责任务的调度,并产生日志及任务调度记录。
无中心化,是指没有调度中心这一概念,每个运行在集群中的作业服务器都是对等的,各个作业节点是自治的、平等的、节点之间通过注册中心进行分布式协调。
**Registry:**以zk作为Elastic-job的注册中心组件,存储了执行任务的相关信息。同时,Elastic-Job利用该组件进行执行任务实例的选举。
**Console:**Elastic-Job提供了运维平台,它通过读取zk数据展现任务执行状态,或更新zk数据修改全局配置。通过Elastic-Job-Lite组件产生的数据来查看任务执行历史记录。
应用程序在启动时,在其内嵌的Elastic-Job-Lite组件会向zk注册该实例的信息,并触发选举(此时可能已经启动了该应用程序的其他实例),从众多实例中选举出来一个Leader,让其执行任务。当到达任务执行时间时,Elastic-Job-Lite组件会调用由应用程序实现的任务业务逻辑,任务执行后会产生任务执行记录。当应用程序的某一个实例宕机时,zk组件会感知到并重新触发leader选举。
2.3.2、Zookeeper
在学习Elastic-Job执行原理时,我们应该先了解一下zk是用来做什么的。因为:
- Elastic-Job依赖Zookeeper完成对执行任务信息的存储(如任务名称、任务参与实例、任务执行策略等)。
- Elastic-Job依赖Zookeeper实现选举机制,在任务执行实例数量变化时(如在快速上手中启动应用新实例或停止实例),会触发选举机制来决定让哪个实例去执行任务。
zk是一个分布式一致性协调服务,它是Apache Hadoop的一个字项目,它主要用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
我们可以把zk想象成一个特殊的数据库,它维护着一个类似文件系统的树形数据结构,zk的客户端(如Elastic-Job任务执行实例)可以对数据进行存取。
每个子目录项如/app1都被称为znode(目录节点),和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除znode,唯一的不同在于znode是可以存储数据的。
ZK之所以被称为一致性协调服务,是因为zk拥有数据监听通知机制,客户端注册监听他关心的znode,当znode发生变化(数据改变、被删除、子目录节点增加删除)时,zk会通知所有的客户端。简单来说就是,当分布式系统的若干个服务都关心一个数据时,当这个数据发生改变,这些服务都能够得知,那么这些服务就针对次数达成了一致。
应用场景思考,使用zk管理分布式配置项的机制:
假设我们的程序时分布式部署在多台机器上,如果我们要改变程序的配置文件,需要逐台机器去修改,非常麻烦,现在我们把这些配置全部放倒zk上去,保存在zk的某个目录节点中,然后所有相关应用程序作为zk的客户端对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到zk的通知,从而获取新的配置信息应用到系统中。
2.3.2.1、Elastic-Job任务信息的保存
Elastic-Job使用zk完成对任务信息的存取,任务执行实例作为zk客户端对其znode操作,任务信息保存在znode中
使用ZooInspector查看zk节点
1、zk图像化客户端工具的下载地址:
https://link.csdn.net/?target=https%3A%2F%2Fissues.apache.org%2Fjira%2Fsecure%2Fattachment%2F12436620%2FZooInspector.zip
2、下载完之后解压压缩包,双击地址为ZooInspector\build\zookeeper-dev-ZooInspector.jar的jar包。
如果双击没反应,首先检查电脑是否配置好了java环境,使用java -jar 再加上这个jar包的路径启动即可
{
"jobName": "file-job",
"jobClass": "org.example.job.FileBackedUpJob",
"jobType": "SIMPLE",
"cron": "0/2 * * * * ?",
"shardingTotalCount": 3,
"shardingItemParameters": "",
"jobParameter": "",
"failover": false,
"misfire": true,
"description": "",
"jobProperties": {
"job_exception_handler": "com.dangdang.ddframe.job.executor.handler.impl.DefaultJobExceptionHandler",
"executor_service_handler": "com.dangdang.ddframe.job.executor.handler.impl.DefaultExecutorServiceHandler"
},
"monitorExecution": true,
"maxTimeDiffSeconds": -1,
"monitorPort": -1,
"jobShardingStrategyClass": "",
"reconcileIntervalMinutes": 10,
"disabled": false,
"overwrite": true
}
节点记录了任务的配置信息,包含执行类、cron表达式、分片算法类、分片数量、分片参数。默认状态下,如果你修改了Job的配置,比如cron表达式,分片数量等是不会更新到zk上去的,需要把LiteJobConfiguration的参数overwrite修改为true,或者删除zk的节点再重新启动作业重新创建。
instances节点:
同一个Job下的elastic-job的部署实例。一台机器上可以启动多个Job实例,也就是jar包。instances的命名是[IP+@-@+PID]。
leader节点:
任务实例的主节点信息,通过zk的主节点选举,选出来的主节点信息。下面的子节点分为election、sharding和failover三个节点,分别用于主节点选举、分片和失效转移处理。election下面的instance节点显示了当前主节点的实例ID。latch节点也是一个永久节点,用于选举时候的实现分布式锁。sharding节点下面有一个临时节点necessary,是否需要重新分片的标记,如果分片总数变化或者任务实例节点上下线,以及主节点选举,都会触发设置重分片标记,主节点会进行分片计算。
sharding节点
任务的分片信息,子节点时分片项序号,从零开撕,至分片总数减一。从这个节点可以看出哪个分片在哪个实例上运行。
2.3.2.2、Elastic-Job任务执行实例选举
Elastic-Job使用zk实现任务执行实例选举,若要使用zk完成选举,就需要了解zk的znode类型了,zk有四种类型的znode,客户端在创建znode时可以指定:
- PERSISTENT-持久化目录节点
客户端创建该类型znode,此客户端与zk断开连接后该节点依旧存在,如果创建了重复的key,比如/data,第二次创建就会失败。
- PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点
客户端与zk断开连接后该节点依旧存在,允许重复创建相同key,zk给该节点名称进行顺序编号,如zk会在后面加一串数字。比如/data/data000001,如果重复创建,会创建一个/data/data000002节点(一直往后加1)
- EPHEMERAL-临时目录节点
客户端与zk断开连接后,该节点被删除,不允许重复创建相同key
- EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点
客户端与zk断开连接后,该节点被删除,允许重复创建相同key,依旧采取顺序编号机制
实例选举实现过程分析
每个Elastic-Job的任务执行实例作为zk的客户端来操作zk的znode
1)任意一个实例启动时首先创建一个/server的PERSISTENT节点
2)多个实例同时创建/server/leader EPHEMERAL子节点
3)/server/leader子节点只能创建一个,后创建的会失败。创建成功的实例被选为leader节点,用来执行任务
4)所有任务实例监听/server/leader的变化,一旦节点被删除,就重新进行选举,抢占式的创建/server/leader节点,谁创建成功谁就是leader。
2.4、小结
通过本章,我们完成了对Elastic-Job技术的快速入门,并了解了Elastic-Job整体架构和工作原理。
对于应用程序,只需要将任务执行细节包装成ElasticJob接口的实现类,并对任务细节进行配置即可完成与Elastic-Job的集成,而Elastic-Job需要依赖zk进行执行任务信息的存取,执行任务实例的选举。通过对快速入门程序的测试,我们可以看到Elastic-Job确实解决了分布式任务调度的核心问题。
2.5、Spring Boot开发分布式任务
2.5.1、集成Spring Boot
将Elastic-job快速入门的例子改造成springboot集成方式
2.5.1.1、导入maven依赖
创建elastic-job-springboot工程,依赖如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>elastic-job-springboot</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-spring</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.name}</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>utf-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.5.1.2、工程结构
2.5.1.3、编写springboot配置文件和启动类
springboot配置文件:
server.port=56081
spring.application.name=task-scheduling-springboot
logging.level.root=info
#数据源定义
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/elastic_job_demo?useUnicode=true
spring.datasource.username=root
spring.datasource.password=root
springboot启动类:
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class,args);
System.out.println("Hello world!");
}
}
2.5.1.4、编写Elastic-Job配置类及任务类
zookeeper配置类
@Configuration
public class ElasticJobRegistryCenterConfig {
//zk端口
private static final int ZOOKEEPER_PORT = 2181;
//zk链接字符串 localhost:2181
private static final String ZOOKEEPER_CONNECTION_STRING = "localhost:" + ZOOKEEPER_PORT;
//定时任务的命名空间
private static final String JOB_NAMESPACE = "elastic-job-example-java";
/**
* 注册中心配置
* @return
*/
@Bean(initMethod = "init")
private static CoordinatorRegistryCenter setUpRegistryCenter() {
//注册中心配置
ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration(ZOOKEEPER_CONNECTION_STRING, JOB_NAMESPACE);
//减少zk的超时时间
zookeeperConfiguration.setSessionTimeoutMilliseconds(600000);
//创建注册中心
CoordinatorRegistryCenter zookeeperRegistryCenter = new ZookeeperRegistryCenter(zookeeperConfiguration);
return zookeeperRegistryCenter;
}
}
Elastic-Job配置类
@Configuration
public class ElasticJobConfig {
@Autowired
private FileBackedUpJob fileBackedUpJob;
@Autowired
private CoordinatorRegistryCenter coordinatorRegistryCenter;
@Bean(initMethod = "init")
public SpringJobScheduler initSimpleElasticJob(){
//创建 SpringJobScheduler
SpringJobScheduler jobScheduler = new SpringJobScheduler(fileBackedUpJob, coordinatorRegistryCenter,
createJobConfiguration(fileBackedUpJob.getClass(),"0/3 * * * * ?",1,null));
return jobScheduler;
}
/**
* 配置任务详细信息
* @param jobClass 任务执行类
* @param cron 执行策略
* @param shardingTotalCount 分片数量
* @param shardingItemParameters 分片个性化参数
* @return
*/
private LiteJobConfiguration createJobConfiguration(final Class<? extends SimpleJob> jobClass,
final String cron,
final int shardingTotalCount,
final String shardingItemParameters){
//定义作业核心配置
JobCoreConfiguration.Builder simpleCoreConfigBuilder = JobCoreConfiguration.newBuilder(jobClass.getName(), cron, shardingTotalCount);
if(!StringUtils.isEmpty(shardingItemParameters)){
simpleCoreConfigBuilder.shardingItemParameters(shardingItemParameters);
}
JobCoreConfiguration jobCoreConfiguration = simpleCoreConfigBuilder.build();
//定义SIMPLE类型配置
SimpleJobConfiguration simpleJobConfiguration = new SimpleJobConfiguration(jobCoreConfiguration, jobClass.getCanonicalName());
//定义Lite作业根配置
LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(simpleJobConfiguration).overwrite(true).build();
return liteJobConfiguration;
}
}
Elastic-Job任务类
使用快速入门中的FileBackupJob类
2.5.2、作业分片
2.5.2.1、分片概念
作业分片是指任务的分布式执行,需要把一个任务拆分为多个独立的任务项,然后由分布式的应用实例分别执行某一个或者几个分片项。
例如:Elastic-Job快速入门中文件备份的例子,现有2台服务器,每台服务器分别跑一个应用实例。为了快速的执行作业,那么可以将作业分成4片,每个应用实例各执行2片。作业遍历数据的逻辑应为:实例1查找text和image类型文件执行备份;实例2查找radio和vedio类型文件执行备份。如果由于服务器扩容应用实例数量增加为4,则作业遍历数据的逻辑应为:4个实例分别处理text、image、radio、video类型的文件。
可以看到,通过对任务合理的分片化,从而达到任务并行处理的效果,最大限度的提高执行作业的吞吐量。
分片项与业务处理解释
Elastic-Job并不直接提供数据处理的功能,框架只会将分片项分配至各个运行中的作业服务器,开发者需要自行处理分片项与真实数据的对应关系。
Elastic-Job只分片及派发任务,数据处理是由程序员来编写和实现的。
最大限度利用资源
将分片项设置为大于服务器的数量,最后是大于服务器倍数的数量,作业将会合理的利用分布式资源,动态的分配分片项。
例如:3台服务器,分成10片,则分片项分配结果为服务器A=0,1,2;服务器B=3,4,5;服务器C=6,7,8,9。如果服务器C崩溃,则分片项分配结果为服务器A=0,1,2,34;服务器B=5,6,7,8,9。在不丢失分片项的情况下,最大限度利用现有资源提高吞吐量。
2.5.2.2、作业分片实现
基于SpringBoot集成方式的而产出的工程代码,完成对作业分片的实现,文件数据备份采取更接近真实项目的数据库存取方式。
2.5.2.2.1、创建数据库
数据库:mysql-5.7.25
创建elastic-job-demo数据库:
create database 'elastic-job-demo' character set 'UTF-8' collate 'utf8_general_ci';
在elastic-job-demo库中创建t_file表:
drop table if exists 't_file';
create table 't_file'(
'id' varchar(11) character set utf8 collate utf8_general_ci not null,
'name' varchar(255) character set utf8 collate utf8_general_ci null default null,
'type' varchar(255) character set utf8 collate utf8_general_ci null default null,
'content' varchar(255) character set utf8 collate utf8_general_ci null default null,
'backedUp' tinyint(1) null default null,
primary key ("id") using btree
) ENGING = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
2.5.2.2.2、新增maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
2.5.2.2.3、编写文件服务类
@Service
public class FileService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 获取某文件类型为备份的文件
* @param fileType 文件类型
* @param count 获取条数
* @return
*/
public List<FileCustom> fetchUnBackUpFiles(String fileType, Integer count){
List<FileCustom> files = jdbcTemplate.query("select * from t_file t where t.type = ? and t.backedUp = 0 order by id limit 0,?"
, new Object[]{fileType, count}
, new BeanPropertyRowMapper(FileCustom.class));
return files;
}
/**
* 备份文件
* @param files 要备份的文件
*/
public void backupFiles(List<FileCustom> files){
for(FileCustom fileCustom : files){
jdbcTemplate.update("update t_file set backedUp = 1 where id = ?",new Object[]{fileCustom.getId()});
System.out.println(String.format("线程 %d | 已备份文件:%s 文件类型:%S",Thread.currentThread().getId(),fileCustom.getName(),fileCustom.getType()));
}
}
}
2.5.2.2.4、编写job任务类
@Component
public class FileBackedUpJobDB implements SimpleJob {
//每次任务要执行的任务数量
private final int FETCH_SIZE = 1;
@Autowired
private FileService fileService;
@Override
public void execute(ShardingContext shardingContext) {
//作业分片信息
int shardingItem = shardingContext.getShardingItem();
System.out.println(String.format("作业分片:%d",shardingItem));
//分片参数 (0=text,1=image,2=radio,3=vedio)
String jobParameter = shardingContext.getShardingParameter();
//1、获取未备份的文件
List<FileCustom> fileCustomList = fetchUnBackUpFiles(jobParameter,FETCH_SIZE);
//2、进行文件备份
backupFiles(fileCustomList);
}
//获取为备份的文件
public List<FileCustom> fetchUnBackUpFiles(String fileType,int count){
List<FileCustom> fileCustomList = fileService.fetchUnBackUpFiles(fileType, count);
//ManagementFactory.getRuntimeMxBean()获取当前JVM进程的PID
System.out.println(String.format("%sTime:%s,已获取%d文件",
ManagementFactory.getRuntimeMXBean().getName(),new SimpleDateFormat("hh:mm:ss").format(new Date()),count));
return fileCustomList;
}
/**
* 备份文件
* @param files
*/
public void backupFiles(List<FileCustom> files){
fileService.backupFiles(files);
}
}
2.5.2.2.5、测试类添加数据
@RunWith(SpringRunner.class)
@SpringBootTest
public class GenerateTestData {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void testGenerateTestData(){
//清除数据
clearTestFiles();
//制造数据
generateTestFiles();
}
private void clearTestFiles() {
jdbcTemplate.update("delete from t_file");
}
private void generateTestFiles() {
List<FileCustom> fileCustomList = new ArrayList<>();
for (int i = 1; i < 11; i++){
fileCustomList.add(new FileCustom(String.valueOf(i+10),"文件"+(i+10),"text","content"+(i+10)));
fileCustomList.add(new FileCustom(String.valueOf(i+10),"文件"+(i+10),"image","content"+(i+10)));
fileCustomList.add(new FileCustom(String.valueOf(i+10),"文件"+(i+10),"radio","content"+(i+10)));
fileCustomList.add(new FileCustom(String.valueOf(i+10),"文件"+(i+10),"vedio","content"+(i+10)));
}
System.out.println("测试数据制造完成!");
for (FileCustom file : fileCustomList){
jdbcTemplate.update("insert into t_file (id,name,type,content,backedUp) values (?,?,?,?,?)",new Object[]{file.getId(),file.getName(),file.getType(),file.getContent(),file.getBackedUp()});
}
}
}
2.5.2.3、作业配置说明
注册中心配置
ZookeeperConfiguration属性详细说明
属性名 | 类型 | 构造器注入 | 缺省值 | 描述 |
---|---|---|---|---|
serverLists | String | 是 | 连接zk服务器的列表 包括IP地址和端口号 多个地址用逗号分隔 如:host1:2181,host2:21881 | |
namespace | String | 是 | zk的命名空间 | |
baseSleepTimeMilliseconds | int | 否 | 1000 | 等待重试的时间间隔的初始值 单位:毫秒 |
maxSleepTimeMilliseconds | String | 否 | 3000 | 等待重试的时间间隔的最大值 单位:毫秒 |
maxRetries | String | 否 | 3 | 最大重试次数 |
sessionTimeoutMilliseconds | boolean | 否 | 60000 | 会话超时时间 单位:毫秒 |
connectionTimeoutMilliseconds | boolean | 否 | 15000 | 连接超时时间 单位:毫秒 |
digest | String | 否 | 连接zk的权限令牌 缺省为不需要权限验证 |
作业配置
作业配置分为3级,分别是JobCoreConfiguration、JobTypeConfiguration和LiteJobConfiguration。
LiteJobConfiguration使用JobTypeConfiguration,JobTypeConfiguration使用JobCoreConfiguration,层层嵌套。
JobTypeConfiguration根据不同实现类型分为SimpleJobConfiguration,DataflowJobConfiguration和ScriptConfiguration。
属性名 | 类型 | 构造器注入 | 缺省值 | 描述 |
---|---|---|---|---|
jobName | String | 是 | 作业名称 | |
cron | String | 是 | cron表达式,用于控制作业触发时间 | |
shardingTotalCount | int | 是 | 作业分片总数 | |
shardingItemParameters | String | 否 | 分片序列号和参数用等号分割,多个键值对用逗号分隔 分片序列号从0开始,不可大于或等于作业分片总数 如:0=a,1=b,2=c | |
jobParameter | String | 否 | 作业自定义参数 可通过传递该参数为作业调度的业务方法传参,用于实现带参数的作业 例:每次获取的数据量、作业实例从数据库读取的主键等 | |
failover | boolean | 否 | false | 是否开启任务执行失效转移,开启表示如果作业在一次任务执行中途宕机,允许将该次未完成的任务在另一任务节点上补偿执行 |
misfire | boolean | 否 | true | 是否开启错过任务重新执行 |
description | String | 否 | 作业描述信息 | |
jobProperties | Enum | 否 | 配置jobProperties定义的枚举控制Elastic-Job的实现细节 JOB_EXCEPPTION_HANDLER用于扩展异常处理类 EXECUTOR_SERVICE_HANDLER用于扩展作业处理线程池类 |
2.5.2.4、作业配置说明
AverageAllocationJobShardingStraregy
全路径:
com.dangdang.ddframe.job.lite.api.strategy.impl.AverageAllocationJobShardingStraregy
策略说明:
基于平均分配算法的分片策略,也是默认的分片策略。
如果分片不能整除,则不能整除的多余分片将依次追加到序号小的服务器。如:
如果有3台服务器,分成9片,则每台服务器分到的分片是:1=[0,1,2];2=[3,4,5];3=[6,7,8]
如果有3台服务器,分成8片,则每台服务器分到的分片是:1=[0,1,6];2=[2,3,7];3=[4,5]
如果有3台服务器,分成10片,则每台服务器分到的分片是:1=[0,1,2,9];2=[3,4,5];3=[6,7,8]
OdevitySortByNameJobShardingStratrgy
全路径:
com.dangdang.ddframe.job.lite.api.strategy.impl.OdevitySortByNameJobShardingStratrgy
策略说明:
根据作业名的哈希值奇偶数决定IP升降序算法的分片策略。
作业名的哈希值为奇数则IP升序。
作业名的哈希值为偶数则IP降序。
用于不同的作业平均分配负载至不同的服务器。
AverageAllocationJobShardingStraregy的缺点是,一旦分片数小于作业服务器数,作业将永远分配至IP地址靠前的服务器,导致IP地址靠后的服务器空闲。而OdevitySortByNameJobShardingStratrgy则可以根据作业名重新分配服务器负载。如:
如果有3台服务器,分成2片,作业名称的哈希值为奇数,则每台服务器分到的分片是:1=[0],2=[1],3=[]
如果有3台服务器,分成2片,作业名称的哈希值为偶数,则每台服务器分到的分片是:1=[],2=[1],3=[0]
RotateServerByNameJobShardingStrategy
全路径:
com.dangdang.ddframe.job.lite.api.strategy.impl.RotateServerByNameJobShardingStrategy
策略说明:
根据作业名的哈希值对服务器列表进行轮转的分片策略。
配置分片策略
与配置通常的作业属性相同,在spring命名空间或者JobConfiguration中配置jobShardingStrategyClass属性,属性值是作业分片策略类的全路径。
分片策略配置xml方式:
<job:simple id="hotelSimpleSpringJob"
class="com.chuanzhi.spiderhotel.job.SpiderJob"
registry-center-ref="regCenter"
cron="0/10 * * * * ?"
sharding-total-count="4"
sharding-item-parameters="0=A,1=B,2=C,4=D"
monitor-port="9888"
reconcile-interval-minutes="10"
job-sharding-strategy-class="com.dangdang.ddframe.job.lite.api.strategy.impl.RotateServerByNameJobShardingStrategy"
/>
分片策略配置java方式
//定义Lite作业根配置
JobRootConfiguration simpleJobRootConfiguration = LiteJobConfiguration.newBuilder(simpleJobConfiguration).jobShardingStrategyClass("com.dangdang.ddframe.job.lite.api.strategy.impl.OdevitySortByNameJobShardingStratrgy").build();
2.5.3、Dataflow类型定时任务
Dataflow类型的定时任务需要实现DataflowJob接口,该接口提供2个方法需要覆盖,分别用于抓取(fetchData)和处理(processData)数据。
Dataflow类型用于处理数据流,它和SimpleJob不同,它以数据流的方式执行,调用fetchData抓取数据,直到抓取不到数据才停止作业。
配置FileBackedUpJobDataFlow
@Component
public class FileBackedUpJobDataFlow implements DataflowJob<FileCustom> {
//每次任务要执行的任务数量
private final int FETCH_SIZE = 1;
@Autowired
private FileService fileService;
@Override
public void execute(ShardingContext shardingContext) {
//作业分片信息
int shardingItem = shardingContext.getShardingItem();
System.out.println(String.format("作业分片:%d",shardingItem));
//分片参数 (0=text,1=image,2=radio,3=vedio)
String jobParameter = shardingContext.getShardingParameter();
//1、获取未备份的文件
List<FileCustom> fileCustomList = fetchUnBackUpFiles(jobParameter,FETCH_SIZE);
//2、进行文件备份
backupFiles(fileCustomList);
}
/**
* 抓取数据
* @param shardingContext
* @return
*/
@Override
public List<FileCustom> fetchData(ShardingContext shardingContext) {
//作业分片信息
int shardingItem = shardingContext.getShardingItem();
System.out.println(String.format("作业分片:%d",shardingItem));
//分片参数 (0=text,1=image,2=radio,3=vedio)
String jobParameter = shardingContext.getShardingParameter();
//1、获取未备份的文件
List<FileCustom> fileCustomList = fetchUnBackUpFiles(jobParameter,FETCH_SIZE);
return fileCustomList;
}
/**
* 处理数据
* @param shardingContext
* @param fileCustomList
*/
@Override
public void processData(ShardingContext shardingContext, List<FileCustom> fileCustomList) {
//2、进行文件备份
backupFiles(fileCustomList);
}
//获取为备份的文件
public List<FileCustom> fetchUnBackUpFiles(String fileType,int count){
List<FileCustom> fileCustomList = fileService.fetchUnBackUpFiles(fileType, count);
//ManagementFactory.getRuntimeMxBean()获取当前JVM进程的PID
System.out.println(String.format("%sTime:%s,已获取%d文件",
ManagementFactory.getRuntimeMXBean().getName(),new SimpleDateFormat("hh:mm:ss").format(new Date()),count));
return fileCustomList;
}
/**
* 备份文件
* @param files
*/
public void backupFiles(List<FileCustom> files){
fileService.backupFiles(files);
}
}
2.6、Elastic-Job高级
2.6.1、事件追踪
Elastic-Job-Lite在配置中提供了JobEventConfiguration,支持数据库方式配置,会在数据库中自动创建JOB_EXECUTION_LOG和JOB_STATUS_TRACE_LOG两张表以及若干索引,来记录作业的相关信息。
2.6.1.1、修改
//需要在ElasticJobConfig中引入数据源
@Autowired
private DataSource datasource;
//然后在init方法中,创建任务事件追踪配置
JobEventRdbConfiguration jobEventRdbConfiguration = new JobEventRdbConfiguration(dataSource);
//再把这个配置加到jobSchedule中
SpringJobScheduler jobScheduler = new SpringJobScheduler(fileBackedUpJob, coordinatorRegistryCenter,
createJobConfiguration(fileBackedUpJob.getClass(),"0/3 * * * * ?",4,"0=text,1=image,2=radio,3=vedio"),jobEventRdbConfiguration);
2.6.1.2、启动项目
启动项目之后,会发现在数据库中会生成两张表。
JOB_EXECUTION_LOG记录每次作业的执行历史。分为两个步骤:
1、作业开始执行时向数据库插入数据,除failure_cause和complete_time外的其他字段均不为空。
2、作业完成执行时向数据库更新数据,更新is_success,complete_time和failure_cause(如果作业执行失败)。
JOB_STATUS_TRACE_LOG记录作业状态变更痕迹表。可通过每次作业运行的task_id查询作业状态变化的生命周期和运行轨迹。
3、xxl-job学习
3.1、概述
3.1.1、什么是任务调度
我们可以思考一下下面业务场景的解决方案:
- 某电商平台需要每天上午10点,下午3点,晚上8点发放一批优惠券
- 某银行系统需要在信用卡到期还款日的前三天进行短信提醒
- 某财务系统需要在每天凌晨0:10分结算前一天的财务数据,统计汇总
以上场景就是任务调度所需要解决的问题
任务调度是为了自动完成特定任务,在约定的特定时刻去执行任务的过程
3.1.2、为什么需要分布式调度
使用Spring中提供的注解@Scheduled,也能实现调度的功能
在业务类中方法中贴上这个注解,然后在启动类上贴上@EnableScheduling
注解
@Scheduled(cron = "0/20 * * * * ? ")
public void doWork(){
//doSomething
}
感觉Spring给我们提供的这个注解可以完成任务调度的功能,好像已经完美解决问题了,为什么还需要分布式呢?
主要有如下这几点原因:
- 高可用:单机版的定式任务调度只能在一台机器上运行,如果程序或者系统出现异常就会导致功能不可用。
- 防止重复执行: 在单机模式下,定时任务是没什么问题的。但当我们部署了多台服务,同时又每台服务又有定时任务时,若不进行合理的控制在同一时间,只有一个定时任务启动执行,这时,定时执行的结果就可能存在混乱和错误了
- 单机处理极限:原本1分钟内需要处理1万个订单,但是现在需要1分钟内处理10万个订单;原来一个统计需要1小时,现在业务方需要10分钟就统计出来。你也许会说,你也可以多线程、单机多进程处理。的确,多线程并行处理可以提高单位时间的处理效率,但是单机能力毕竟有限(主要是CPU、内存和磁盘),始终会有单机处理不过来的情况。
3.1.3、XXL-JOB介绍
XXL-JOB:是大众点评的分布式任务调度平台,是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。
大众点评目前已经接入XXL_JOB,该系统在内部已调度约100万次,表现优异。
目前已有多家公司接入xxl-job,包括比较知名的大众点评、京东、优信二手车、360金融(360)、联想集团、易信(网易)等等。
官网地址:https://www.xuxueli.com/xxl-job/
系统架构图
执行器相当于我们的应用程序,需要我们去添加XXL-JOB的依赖,添加xxl的任务注解,编写任务的处理逻辑,在配置文件中配置调度中心的地址(启动的时候注册信息)。
当执行器启动的时候,会向调度中心注册执行器的信息。
调度中心拥有执行器的地址和端口,可以配置任务的信息(Cron表达式),当达到任务的时间,由调度中心调用执行器的方法,收集任务的执行情况,有可视化界面展示任务执行的情况。调度中心是独立于执行器之外,需要单独部署的程序
调度中心和执行器之间实现了远程调用的功能。任务开始时,调度中心获取到执行器的地址和端口,通过注解找到任务类进行执行任务;任务执行结束之后,会反馈给调度中心,调度中心可以收集任务的执行情况,可视化界面展示任务的执行情况。
设计思想
将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。
将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。
因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性;
3.2、快速入门
3.2.1、下载源码
源码下载地址:
https://github.com/xuxueli/xxl-job
https://gitee.com/xuxueli0323/xxl-job
3.2.2、初始化调度数据库
下载项目源码并解压,获取“调度数据库初始化SQL脚本”并执行即可。
“调度数据库初始化SQL脚本”位置为:
/xxl-job/doc/db/tables_xxl_job.sql
3.2.3、编译源码
解压源码之后,按maven格式将源码导入IDE,使用maven进行编译即可,源码结构如下:
3.3、配置部署调度中心
3.3.1、调度中心配置
修改xxl-job-admin项目的配置文件application.properties,把数据库账号密码配置上
### xxl-job-admin 就是调度中心
### web
server.port=8080
server.servlet.context-path=/xxl-job-admin
### actuator
management.server.servlet.context-path=/actuator
management.health.mail.enabled=false
### resources
spring.mvc.servlet.load-on-startup=0
spring.mvc.static-path-pattern=/static/**
spring.resources.static-locations=classpath:/static/
### freemarker
spring.freemarker.templateLoaderPath=classpath:/templates/
spring.freemarker.suffix=.ftl
spring.freemarker.charset=UTF-8
spring.freemarker.request-context-attribute=request
spring.freemarker.settings.number_format=0.##########
### mybatis
mybatis.mapper-locations=classpath:/mybatis-mapper/*Mapper.xml
#mybatis.type-aliases-package=com.xxl.job.admin.core.model
### xxl-job, datasource
spring.datasource.url=jdbc:mysql://192.168.202.200:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=WolfCode_2017
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
### datasource-pool
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=HikariCP
spring.datasource.hikari.max-lifetime=900000
spring.datasource.hikari.connection-timeout=10000
spring.datasource.hikari.connection-test-query=SELECT 1
spring.datasource.hikari.validation-timeout=1000
### xxl-job 如果执行失败就会发送邮箱
### xxl-job, email
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
spring.mail.from=xxx@qq.com
spring.mail.password=xxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
### 执行器连接调度中心的时候,需要相同的accessToken
### xxl-job, access token
xxl.job.accessToken=default_token
### xxl-job, i18n (default is zh_CN, and you can choose "zh_CN", "zh_TC" and "en")
xxl.job.i18n=zh_CN
## xxl-job, triggerpool max size
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100
### xxl-job, log retention days
xxl.job.logretentiondays=30
3.3.2、部署项目
运行XxlJobAdminApplication程序即可。
调度中心访问地址http:// 服务器 I P : {服务器IP}: 服务器IP:{server.port}/${server.servlet.context-path}
默认登录账号“admin/123456”,登录后的运行界面如下图所示
至此,“调度中心”项目已经部署成功。
3.4、配置部署执行器项目
3.4.1、添加Maven依赖
创建springboot项目,并添加如下依赖:
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.1</version>
</dependency>
3.4.2、执行器配置
在配置文件中添加如下配置:
### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### 执行器通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=default_token
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=xxl-job-executor-sample
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
xxl.job.executor.address=
### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
xxl.job.executor.ip=127.0.0.1
### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
xxl.job.executor.port=9999
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
xxl.job.executor.logretentiondays=30
3.4.3、添加执行器配置
@Configuration
public class XxlJobConfig {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.address}")
private String address;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}
3.4.4、添加任务处理类
添加任务处理类,交给Spring容器管理,在处理方法上贴上@XxlJob注解
@Component
public class SimpleXxlJob {
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {
System.out.println("执行定时任务,执行时间:"+new Date());
}
}
3.5、运行HelloWord程序
3.5.1、任务配置&触发执行
调度中心和任务是分离开的,任务只是定义了我们需要做什么事,究竟什么时候做、时间的分配、时间的定义,是由调度中心定义的。
登录调度中心,在任务管理中新增任务,配置内容如下:
新增后界面如下:
接着启动定时调度任务:
执行一次:只执行一次任务;
启动:一直执行任务,知道调度中心停止此任务;
3.5.2、查看日志
在调度中心的调度日志就可以看到任务的执行结果:
同时IDE的控制台也可以看见打印的日志
3.6、GLUE模式(Java)
任务以源码方式维护在调度中心,支持通过Web IDE在线更新,实时编译和生效,因此不需要指定JobHandler。
“GLUE模式(Java)”运行模式的任务实际上是一段继承自IJobHandler的Java类代码,它在执行器项目中运行,可使用@Resource/@AUtowire注入执行器里的其他服务。
但是值得注意的是,引入服务的时候,必须要手动import该服务所在的package。
添加Service
@Service
public class HelloService {
public void methodA(){
System.out.println("执行MethodA的方法");
}
public void methodB(){
System.out.println("执行MethodB的方法");
}
}
添加任务配置
通过GLUE IDE在线编辑代码
编写内容如下:
package com.xxl.job.service.handler;
import cn.wolfcode.xxljobdemo.service.HelloService;
import com.xxl.job.core.handler.IJobHandler;
import org.springframework.beans.factory.annotation.Autowired;
public class DemoGlueJobHandler extends IJobHandler {
@Autowired
private HelloService helloService;
@Override
public void execute() throws Exception {
helloService.methodA();
}
}
启动并执行程序
3.6、集群环境搭建
启动两个SpringBoot程序,需要修改Tomcat端口和执行器端口
- Tomcat端口8088程序的命令行参数如下:
-Dserver.port=8088 -Dxxl.job.executor.port=9998
- Tomcat端口8089程序的命令行参数如下:
-Dserver.port=8089 -Dxxl.job.executor.port=9999
在任务管理中,修改路由策略,修改成轮询
重新启动,我们发现定时任务会在这两台机器上进行轮询的执行
3.6.2、调度路由算法讲解
当执行器集群部署时,提供丰富的路由策略,包括:
1、FiRST(第一个):固定选择第一个机器
2、LAST(最后一个):固定选择最后一个机器
3、ROUND(轮询):依次选择在线的机器发起调度
4、RANDOM(随机):随机选择在线的机器
5、CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上
6、LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举
7、LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举
8、FAULOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度
9、BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度
10、SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务
3.7、分片功能讲解
3.7.1、案例需求讲解
需求:我们现在实现这样的需求,在指定节假日,需要给平台的所有用户去发送祝福的短信。
3.7.1.1、初始化数据
在数据库中导入xxl_job_demo.sql
数据
3.7.1.2、集成Druid&MyBatis
添加依赖
<!--MyBatis驱动-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
添加配置
spring.datasource.url=jdbc:mysql://localhost:3306/xxl_job_demo?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.username=root
spring.datasource.password=WolfCode_2017
添加实体类
@Setter@Getter
public class UserMobilePlan {
private Long id;//主键
private String username;//用户名
private String nickname;//昵称
private String phone;//手机号码
private String info;//备注
}
添加Mapper处理类
@Mapper
public interface UserMobilePlanMapper {
@Select("select * from t_user_mobile_plan")
List<UserMobilePlan> selectAll();
}
3.7.1.3、业务功能实现
任务处理方法实现
@XxlJob("sendMsgHandler")
public void sendMsgHandler() throws Exception{
List<UserMobilePlan> userMobilePlans = userMobilePlanMapper.selectAll();
System.out.println("任务开始时间:"+new Date()+",处理任务数量:"+userMobilePlans.size());
Long startTime = System.currentTimeMillis();
userMobilePlans.forEach(item->{
try {
//模拟发送短信动作
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("任务结束时间:"+new Date());
System.out.println("任务耗时:"+(System.currentTimeMillis()-startTime)+"毫秒");
}
任务配置信息
3.7.2、分片概念讲解
比如我们的案例中有2000+条数据,如果不采取分片形式的话,任务只会在一台机器上执行,这样的话需要20+秒才能执行完任务。
如果采取分片广播的形式的话,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,同时系统自动传递分片参数,可根据分片参数开发分片任务;
获取分片参数的方式:
// 可参考Sample示例执行器中的示例任务"ShardingJobHandler"了解试用
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
通过这两个参数,我们可以通过求模取余的方式,分别查询,分别执行,这样的话就可以提高处理的速度。
之前2000+条数据只在一台机器上执行需要20+秒才能完成任务,分片后,有两台机器可以共同完成2000+条数据,每台机器处理1000+条数据,这样的话只需要10+秒就能完成任务。
3.7.3、案例改造成任务分片
Mapper增加查询方法
@Mapper
public interface UserMobilePlanMapper {
@Select("select * from t_user_mobile_plan where mod(id,#{shardingTotal})=#{shardingIndex}")
List<UserMobilePlan> selectByMod(@Param("shardingIndex") Integer shardingIndex,@Param("shardingTotal")Integer shardingTotal);
@Select("select * from t_user_mobile_plan")
List<UserMobilePlan> selectAll();
}
任务类方法
@XxlJob("sendMsgShardingHandler")
public void sendMsgShardingHandler() throws Exception{
System.out.println("任务开始时间:"+new Date());
int shardTotal = XxlJobHelper.getShardTotal();
int shardIndex = XxlJobHelper.getShardIndex();
List<UserMobilePlan> userMobilePlans = null;
if(shardTotal==1){
//如果没有分片就直接查询所有数据
userMobilePlans = userMobilePlanMapper.selectAll();
}else{
userMobilePlans = userMobilePlanMapper.selectByMod(shardIndex,shardTotal);
}
System.out.println("处理任务数量:"+userMobilePlans.size());
Long startTime = System.currentTimeMillis();
userMobilePlans.forEach(item->{
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("任务结束时间:"+new Date());
System.out.println("任务耗时:"+(System.currentTimeMillis()-startTime)+"毫秒");
}
任务设置
3.8、项目集成XXL-JOB
3.8.1、需要集成xxl的pom引入xxl-job-core的依赖,把xxl-job的配置信息放到配置文件中
3.8.2、需要在项目中加入xxlConfig的配置类,让xxljob可以让spring进行管理
3.8.3、需要在xxl-job的调度中心中进行配置执行器信息,然后添加一个job执行类
4、转换方案
4.1、添加xxl-job的配置信息,注释elastic-job的配置信息
4.2、添加xxl-job的配置类,编写job任务类,注意加锁、注释
开发步骤:
1、任务开发:在Spring Bean实例中,开发Job方法;
2、注解配置:为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。
3、执行日志:需要通过 "XxlJobHelper.log" 打印执行日志;
4、任务结果:默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果;