XXL开源社区
一、传统的定时任务
1. 传统的定时任务存在那些缺点

传统任务调度存在的缺陷

  • 1.业务逻辑与定时任务逻辑放入在同一个Jar包中,如果定时任务逻辑挂了也会影响到业务逻辑;
  • 2.如果服务器集群的情况下,可能存在定时任务逻辑会重复触发执行;
  • 3.定时任务执行非常消耗cpu的资源,可能会影响到业务线程的执行
2. 分布式任务调度

分布式集群的模式下,如果采用集中式的任务调度方式,会带来一些问题,比如

  • 1、多台机器集群部署的定时任务如何保证不被重复执行?
  • 2、如何动态地调整定时任务的执行时间?(不重启服务的情况)
  • 3、部署定时任务的机器发生故障如何实现故障转移?
  • 4、如何对定时任务进行监控、告警、高可用?
  • 5、定时任务统一调度管理,和追踪各个的服务节点之间任务调度的结
    果,并保存记录任务信息等等
3. 定时任务集群幂等性问题

定时任务集群,如何保证定时任务幂等性问题
如何在集群中,保证我们的定时任务只会触发一次

  • 1.将业务逻辑和定时任务逻辑完全分开部署,实现解耦、只对业务逻辑实现集群,不对我们的定时任务逻辑集群;—定时任务单机版本 缺点无法实现高可用的问题;
  • 2.对我们Jar包加上一个开关,项目启动的时候读取该开关 如果为true的情况下则加载定时任务类,否则情况下就不加载该定时任务类;–缺点无法实现高可用的问题;
  • 3.在数据库加上一个主键能够创建成功,则触发定时任务,否则就不触发定时任务, 高可用的问题;
  • 4.分布式锁实实现,只要jar能够拿到分布式锁就能够执行定时任务,否则情况下不执行;

总结:以上的方案都是属于规模比较小的项目,在微服务架构中应该采用分布式任务调度平台。

二、传统定时任务的实现方案

多线程形式、timetask、线程池(ScheduledExecutorService)、SpringBoot注解形式(EnableScheduling +@Scheduled底层依然是采用 Spring Task )、quartz

2.1. 多线程

基于多线程方式实现

package com.gblfy;

public class ThreadTask {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(1000);
                        System.out.println("定时任务触发...");
                    } catch (InterruptedException e) {

                    }
                }
            }
        }).start();
    }
}

2.2. TimeTask
package com.gblfy;

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

public class TimerTaskDemo {
    public static void main(String[] args) {
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "定时任务触发");
            }
        };
        // 天数
        long delay = 0;
        // 耗秒数
        long period = 1000;
        new Timer().scheduleAtFixedRate(timerTask, delay, period);
    }
}

2.3. 线程池
package com.gblfy;

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

public class ScheduledExecutorServiceDemo {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("定时任务触发..");
            }
        };
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        scheduledExecutorService.scheduleAtFixedRate(runnable, 1, 1, TimeUnit.SECONDS);
    }
}

2.4. SpringBoot注解形式
package com.gblfy;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
public class ScheduledTaskApplication {

    public static void main(String[] args) {
        SpringApplication.run(ScheduledTaskApplication.class, args);
    }

}

package com.gblfy;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class UserScheduled {
    @Scheduled(cron = "0/1 * * * * *")
    public void taskUserScheduled() {
        System.out.println("定时任务触发...");
    }

}

2.5. 基于Quartz
  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- quartz -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz-jobs</artifactId>
            <version>2.3.2</version>
        </dependency>

MyJob

package com.gblfy.job;

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

public class MyJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
       System.out.println("quartz MyJob date:" + System.currentTimeMillis());
   }
}

QuartzTest

package com.gblfy.job;

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

import java.util.Date;

public class QuartzTest {
    public static void main(String[] args) throws SchedulerException {
        //1.创建Scheduler的工厂
        SchedulerFactory sf = new StdSchedulerFactory();
        //2.从工厂中获取调度器实例
        Scheduler scheduler = sf.getScheduler();


        //3.创建JobDetail
        JobDetail jb = JobBuilder.newJob(MyJob.class)
                .withDescription("this is a ram job") //job的描述
                .withIdentity("ramJob", "ramGroup") //job 的name和group
                .build();

        //任务运行的时间,SimpleSchedle类型触发器有效
        long time = System.currentTimeMillis() + 3 * 1000L; //3秒后启动任务
        Date statTime = new Date(time);

        //4.创建Trigger
        //使用SimpleScheduleBuilder或者CronScheduleBuilder
        Trigger t = TriggerBuilder.newTrigger()
                .withDescription("")
                .withIdentity("ramTrigger", "ramTriggerGroup")
                //.withSchedule(SimpleScheduleBuilder.simpleSchedule())
                .startAt(statTime)  //默认当前时间启动
                .withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?")) //两秒执行一次
                .build();

        //5.注册任务和定时器
        scheduler.scheduleJob(jb, t);

        //6.启动 调度器
        scheduler.start();

    }
}
三、常⻅分布式定时任务
3.1. Quartz

Quartz关注点在于定时任务⽽并⾮是数据,并没有⼀套根据数据化处理⽽定的流程
虽然可以实现数据库作业的⾼可⽤,但是缺少了分布式的并⾏调度功能,相对弱点
不⽀持任务分⽚、没UI界⾯管理,并⾏调度、失败策略等也缺少

3.2. TBSchedule

这个是阿⾥早期开源的分布式任务调度系统,使⽤的是timer⽽不是线程池执⾏任务调度,使⽤timer在处理异常的时候是有缺陷的,但TBSchedule的作业类型⽐较单⼀,⽂档也缺失得⽐较严重
⽬前阿⾥内部使⽤的是ScheduleX
阿⾥云也有商业化版本: https://help.aliyun.com/product/147760.html

3.3. Elastic-job

当当开发的分布式任务调度系统,功能强⼤,采⽤的是zookeeper实现分布式协调,具有⾼可⽤与分⽚。
2020年6⽉,ElasticJob的四个⼦项⽬已经正式迁⼊Apache仓库由 2 个相互独⽴的⼦项⽬ ElasticJob-Lite 和 ElasticJobCloud 组成ElasticJob-Lite 定位为轻量级⽆中⼼化解决⽅案,使⽤jar的形式提供分布式任务的协调服务;ElasticJob-Cloud 使⽤ Mesos 的解决⽅案,额外提供资源治理、应⽤分发以及进程隔离等服务
地址:https://shardingsphere.apache.org/elasticjob/index_zh.html

3.4. XXL-JOB

⼤众点评的员⼯徐雪⾥在15年发布的分布式任务调度平
台,是轻量级的分布式任务调度框架,⽬标是开发迅速、
简单、清理、易扩展; ⽼版本是依赖quartz的定时任务触
发,在v2.1.0版本开始 移除quartz依赖
地址:https://www.xuxueli.com/xxl-job

四、分布式任务调度平台架构设计原理
4.1. xxl-job项目模块讲解

xxl-job-admin—分布式任务调度中心平台
xxl-job-core—源码实现部分
xxl-job-executor-samples–执行器项目 定时任务模块项目

执行器模块:(注册中心)存放实际执行我们定时任务项目模块IP和端口信息;
分布式任务调度中心(Nginx):负责所有执行器执行定时任务的分配;

4.2. 最佳实战

定时任务与业务逻辑实现解耦,分开部署。
定时任务是一个单独的项目。
gblfy-member—会员服务接口
member-job----负责会员服务定时任务

4.3. Xxl-job定时任务框架实现原理
  1. 当我们的定时任务模块项目启动的时候,会将该ip和端口信息注册到 定时任务注册中心上并发送rest请求
  2. 需要将定时任务创建在任务调度中心中,关联执行器 定时任务模块实际执行ip和端口号码。
  3. 创建定时任务会在xxl-job admin 调度中心中项目先触发,从执行器注册中心查找到执行器接口信息,采用路由策略(负载均衡算法)选择一个执行器(定时任务)地址 发送通知执行定时任务。

建议:配置定时任务规则的时候,建议提前5-10s;


常用分布式任务调度框架
Xxl-job、elasticjob、SpringAlibaba Cloud SchedulerX
XXL-Job Admin如何实现集群

五、常见面试题
5.1. XXL-JOB与ElasticJob区别之间区别

XXL-JOB内置定时任务调度中心,ElasticJob借助于zookeeper作为注册中心

对⽐项XXL-JOBelastic-job
并⾏调度调度系统多线程并⾏任务分⽚的⽅式并⾏
弹性扩容使⽤Quartz基于数据库分布式功能通过zookeeper保证
⾼可⽤通过DB锁保证通过zookeeper保证
阻塞策略单机串⾏/丢弃后续的调度/覆盖之前的调度执⾏超过zookeeper的session timeout
时间的话,会被清除,重新进⾏分⽚
动态分⽚策略以执⾏器为维度进⾏分⽚、⽀持动态的扩容平均分配/作业名hash分配/⾃定义策略
失败处理策略失败告警/失败重试执⾏完毕后主动获取未分配分⽚任务,
服务器下线后主动寻找可以⽤的服务器执⾏任务
监控⽀持⽀持
⽇志⽀持⽀持
5.2. 分布式任务调度分片集群策略原理

执行器集群部署时,任务路由策略选择”分片广播”情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;

“分片广播” 以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。

“分片广播” 和普通任务开发流程一致,不同之处在于可以获取分片参数,获取分片参数进行分片业务处理。

5.3. 如何保证任务调度平台高可用问题

调度中心集群(可选):
调度中心支持集群部署,提升调度系统容灾和可用性。

调度中心集群部署时,几点要求和建议:

  • 1.DB配置保持一致;

  • 2.集群机器时钟保持一致(单机集群忽视);

  • 建议:推荐通过nginx为调度中心集群做负载均衡,分配域名。调度中心访问、执行器回调配置、调用API服务等操作均通过该域名进行。

执行器集群(可选):
执行器支持集群部署,提升调度系统可用性,同时提升任务处理能力。
执行器集群部署时,几点要求和建议:

  • 1.执行器回调地址(xxl.job.admin.addresses)需要保持一致;执行器根据该配置进行执行器自动注册等操作。
  • 2.同一个执行器集群内AppName(xxl.job.executor.appname)需要保持一致;调度中心根据该配置动态发现不同集群的在线执行器列表。
六、XXL-Job集群部署和⾼可⽤最佳实战