elastic-job转成xxl-job

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表达式

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属性详细说明

属性名类型构造器注入缺省值描述
serverListsString连接zk服务器的列表 包括IP地址和端口号 多个地址用逗号分隔 如:host1:2181,host2:21881
namespaceStringzk的命名空间
baseSleepTimeMillisecondsint1000等待重试的时间间隔的初始值 单位:毫秒
maxSleepTimeMillisecondsString3000等待重试的时间间隔的最大值 单位:毫秒
maxRetriesString3最大重试次数
sessionTimeoutMillisecondsboolean60000会话超时时间 单位:毫秒
connectionTimeoutMillisecondsboolean15000连接超时时间 单位:毫秒
digestString连接zk的权限令牌 缺省为不需要权限验证

作业配置

作业配置分为3级,分别是JobCoreConfiguration、JobTypeConfiguration和LiteJobConfiguration。

LiteJobConfiguration使用JobTypeConfiguration,JobTypeConfiguration使用JobCoreConfiguration,层层嵌套。

JobTypeConfiguration根据不同实现类型分为SimpleJobConfiguration,DataflowJobConfiguration和ScriptConfiguration。

属性名类型构造器注入缺省值描述
jobNameString作业名称
cronStringcron表达式,用于控制作业触发时间
shardingTotalCountint作业分片总数
shardingItemParametersString分片序列号和参数用等号分割,多个键值对用逗号分隔 分片序列号从0开始,不可大于或等于作业分片总数 如:0=a,1=b,2=c
jobParameterString作业自定义参数 可通过传递该参数为作业调度的业务方法传参,用于实现带参数的作业 例:每次获取的数据量、作业实例从数据库读取的主键等
failoverbooleanfalse是否开启任务执行失效转移,开启表示如果作业在一次任务执行中途宕机,允许将该次未完成的任务在另一任务节点上补偿执行
misfirebooleantrue是否开启错过任务重新执行
descriptionString作业描述信息
jobPropertiesEnum配置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. 高可用:单机版的定式任务调度只能在一台机器上运行,如果程序或者系统出现异常就会导致功能不可用。
  2. 防止重复执行: 在单机模式下,定时任务是没什么问题的。但当我们部署了多台服务,同时又每台服务又有定时任务时,若不进行合理的控制在同一时间,只有一个定时任务启动执行,这时,定时执行的结果就可能存在混乱和错误了
  3. 单机处理极限:原本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" 自主设置任务结果;

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值