1 什么是任务调度

我们可以先思考一下下面业务场景的解决方案:

  • 某电商系统需要在每天上午10点,下午3点,晚上8点发放一批优惠券。
  • 某银行系统需要在信用卡到期还款日的前三天进行短信提醒。
  • 某财务系统需要在每天凌晨0:10结算前一天的财务数据,统计汇总。
  • 12306会根据车次的不同,而设置某几个时间点进行分批放票。
  • 某网站为了实现天气实时展示,每隔5分钟就去天气服务器获取最新的实时天气信息。

以上场景就是任务调度所需要解决的问题。

任务调度是指系统为了自动完成特定任务,在约定的特定时刻去执行任务的过程。有了任务调度即可解放更多的人 力由系统自动去执行任务。

任务调度如何实现?

多线程方式实现:

学过多线程的同学,可能会想到,我们可以开启一个线程,每sleep一段时间,就去检查是否已到预期执行时间。

以下代码简单实现了任务调度的功能:

package org.example.simpleDemo;

public class Runtest {
    public static void main(String[] args) {
        //任务执行间隔时间
        final long timeInterval = 1000;
        Runnable runnable = new Runnable() {
            public void run() {
                while (true) {
                    //TODO:something
                    System.out.println("博主很帅!");
                    try {
                        Thread.sleep(timeInterval);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

上面的代码实现了按一定的间隔时间执行任务调度的功能。

Jdk也为我们提供了相关支持,如Timer、ScheduledExecutor,下边我们了解下。

Timer方式实现

package org.example.simpleDemo;

import java.util.Timer;
import java.util.TimerTask;

public class Timertest {
    public static void main(String[] args){
        Timer timer = new Timer();
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                //TODO:something
                System.out.println("博主很帅!!");
            }
        }, 1000, 2000); //1秒后开始调度,每2秒执行一次
    }
}

Timer 的优点在于简单易用,每个Timer对应一个线程,因此可以同时启动多个Timer并行执行多个任务,同一个Timer中的任务是串行执行。

ScheduledExecutor方式实现:

package org.example.simpleDemo;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledExecutortest {
    public static void main(String [] agrs){
        ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
        service.scheduleAtFixedRate(
                new Runnable() {
                    @Override
                    public void run() {
                        //TODO:something
                        System.out.println("博主很帅");
                    }
                }, 1,
                2, TimeUnit.SECONDS);
    }
}

Java 5 推出了基于线程池设计的 ScheduledExecutor,其设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。

Timer 和 ScheduledExecutor 都仅能提供基于开始时间与重复间隔的任务调度,不能胜任更加复杂的调度需求。比如,设置每月第一天凌晨1点执行任务、复杂调度任务的管理、任务间传递数据等等。

Quartz 是一个功能强大的任务调度框架,它可以满足更多更复杂的调度需求,Quartz 设计的核心类包括 Scheduler, Job 以及 Trigger。其中,Job负责定义需要执行的任务,Trigger负责设置调度策略,Scheduler 将二者组装在一起,并触发任务开始执行。Quartz支持简单的按时间间隔调度、还支持按日历调度方式,通过设置CronTrigger表达式(包括:秒、分、时、日、月、周、年)进行任务调度。

package org.example.simpleDemo;

import org.quartz.Job;
import org.quartz.JobExecutionContext;

public class MyJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext){
        //TODO something
        System.out.println("博主很帅");
    }
}
package org.example.simpleDemo;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

public class SchedulerFactorytest {
    public static void main(String[] agrs) 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();
    }
}

通过以上内容我们学习了什么是任务调度,任务调度所解决的问题,以及任务调度的多种实现方式。

2 什么是分布式任务调度

什么是分布式?

当前软件的架构正在逐步转变为分布式架构,将单体结构分为若干服务,服务之间通过网络交互来完成用户的业务处理,如下图,电商系统为分布式架构,由订单服务、商品服务、用户服务等组成:

分布式任务调度架构怎么设计 分布式任务调度场景_Elastic

分布式系统具体如下基本特点:

  1. 分布性:每个部分都可以独立部署,服务之间交互通过网络进行通信,比如:订单服务、商品服务。
  2. 伸缩性:每个部分都可以集群方式部署,并可针对部分结点进行硬件及软件扩容,具有一定的伸缩能力。
  3. 高可用:每个部分都可以集群部分,保证高可用。

什么是分布式调度?

通常任务调度的程序是集成在应用中的,比如:优惠卷服务中包括了定时发放优惠卷的的调度程序,结算服务中包 括了定期生成报表的任务调度程序,由于采用分布式架构,一个服务往往会部署多个冗余实例来运行我们的业务,在这种分布式系统环境下运行任务调度,我们称之为分布式任务调度,如下图:

分布式任务调度架构怎么设计 分布式任务调度场景_任务调度_02

分布式调度要实现的目标:

不管是任务调度程序集成在应用程序中,还是单独构建的任务调度系统,如果采用分布式调度任务的方式就相当于将任务调度程序分布式构建,这样就可以具有分布式系统的特点,并且提高任务的调度处理能力:

1、并行任务调度

并行任务调度实现靠多线程,如果有大量任务需要调度,此时光靠多线程就会有瓶颈了,因为一台计算机CPU的处理能力是有限的。

如果将任务调度程序分布式部署,每个结点还可以部署为集群,这样就可以让多台计算机共同去完成任务调度,我们可以将任务分割为若干个分片,由不同的实例并行执行,来提高任务调度的处理效率。

2、高可用

若某一个实例宕机,不影响其他实例来执行任务。

3、弹性扩容

当集群中增加实例就可以提高并执行任务的处理效率。

4、任务管理与监测

对系统中存在的所有定时任务进行统一的管理及监测。让开发人员及运维人员能够时刻了解任务执行情况,从而做出快速的应急处理响应。

5、避免任务重复执行

当任务调度以集群方式部署,同一个任务调度可能会执行多次,比如在上面提到的电商系统中到点发优惠券的例子,就会发放多次优惠券,对公司造成很多损失,所以我们需要控制相同的任务在多个运行实例上只执行一次,考虑采用下边的方法:

  • 分布式锁,多个实例在任务执行前首先需要获取锁,如果获取失败那么久证明有其他服务已经再运行,如果获取成功那么证明没有服务在运行定时任务,那么就可以执行。

分布式任务调度架构怎么设计 分布式任务调度场景_任务调度_03

 

  • ZooKeeper选举,利用ZooKeeper对Leader实例执行定时任务,有其他业务已经使用了ZK,那么执行定时任务的时候判断自己是否是Leader,如果不是则不执行,如果是则执行业务逻辑,这样也能达到我们的目的。

分布式任务调度架构怎么设计 分布式任务调度场景_分布式_04

 

3 Elastic-Job介绍

针对分布式任务调度的需求市场上出现了很多的产品:

1)Elastic-job:当当网基于quartz 二次开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,实现任务高可用以及分片。

2)Saturn:

唯品会开源的一个分布式任务调度平台,可以全域统一配置,统一监控,任务高可用以及分片并发处理。它是在elastic-job基础之上改良出来的。

3)xxl-job:大众点评的分布式任务调度平台,是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

4)TBSchedule:淘宝的一款非常优秀的高性能分布式调度框架,目前被应用于阿里、京东、支付宝、国美等很多互联网企业的流程调度系统中。

Elastic-Job是一个分布式调度的解决方案,由当当网开源,它由两个相互独立的子项目Elastic-Job-Lite和ElasticJob-Cloud组成,使用Elastic-Job可以快速实现分布式任务调度。

Elastic-Job的github地址:https://github.com/elasticjob

功能列表:

分布式调度协调

在分布式环境中,任务能够按指定的调度策略执行,并且能够避免同一任务多实例重复执行。

丰富的调度策略

基于成熟的定时任务作业框架Quartz cron表达式执行定时任务。

弹性扩容缩容

当集群中增加某一个实例,它应当也能够被选举并执行任务;当集群减少一个实例时,它所执行的任务能被转移到别的实例来执行。

失效转移

某实例在任务执行失败后,会被转移到其他实例执行。

错过执行作业重触发

若因某种原因导致作业错过执行,自动记录错过执行的作业,并在上次作业完成后自动触发。

支持并行调度

支持任务分片,任务分片是指将一个任务分为多个小任务项在多个实例同时执行。

作业分片一致性

当任务被分片后,保证同一分片在分布式环境中仅一个执行实例。

支持作业生命周期操作

可以动态对任务进行开启及停止操作。

丰富的作业类型

支持Simple、DataFlow、Script三种作业类型,后续会有详细介绍。

Spring整合以及命名空间支持

对Spring支持良好的整合方式,支持spring自定义命名空间,支持占位符。

运维平台

提供运维界面,可以管理作业和注册中心。