22-09-29 西安 谷粒商城(07)定时任务、SpringTask异步任务、xxl-job

SpringTask异步调用

在一些场景下,我们会希望异步调用,比如添加商品到购物车,修改购物项的数量、删除购物项等等。

1、依赖引入与配置

引入依赖:因为 SpringTask 是 Spring Framework 的模块,无需引入依赖。

spring:
  task:
    execution:
      thread-name-prefix: async-task- # 线程池的线程名的前缀。默认为 task- ,建议根据自己应用来设置
      pool: # 线程池相关
        queue-capacity: 30 # 任务队列长度,用来缓冲执行任务的队列的大小,默认Integer.MAX_VALUE
        keep-alive: 60S # 空闲线程的存活时间,默认为 60 秒
        core-size: 8  # 核心线程数,线程池创建时候初始化的线程数 默认8
        max-size: 100 # 最大线程数,默认 maxSize = Integer.MAX_VALUE;
        allow-core-thread-timeout: true # 是否允许核心线程超时,即开启线程池的动态增长和缩小。默认为 true
      shutdown:
        await-termination: true # 是否等待正在执行还未完成的任务执行后再关闭线程  默认false,建议设置为 true
        await-termination-period: 30  # 等待未完成任务执行的时间,单位为秒。默认为 0 

使用 Spring Task 的异步任务,一定要注意三个点:

  • 配置线程池控制线程及阻塞队列的大小。

  • JVM 应用的正常优雅关闭,保证异步任务都被执行完成。

  • 编写异步异常处理器(实现AsyncUncaughtExceptionHandler接口),记录异常日志,进行监控告警。


2、SpringTask入门案例

启动类上加@EnableAsync,在springboot工程的启动类上添加@EnableAsync开启spring-task的异步功能

@EnableAsync
@SpringBootApplication
public class SpringBootDemoAsyncApplication {

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

}

@Async标记异步调用方法

在CartAsyncService中编写俩个测试方法executor1和executor2

注意,这2个方法executor1睡眠4s,executor2睡眠5秒,

@Service
public class CartAsyncService {
    @Async
    public String executor1() {
        try {
            System.out.println("executor1方法开始执行");
            //这个方法执行需要4秒
            TimeUnit.SECONDS.sleep(4);
            System.out.println("executor1方法结束执行。。。");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "executor1";
    }

    @Async
    public String executor2() {
        try {
            System.out.println("executor2方法开始执行");
            //这个方法执行需要5秒
            TimeUnit.SECONDS.sleep(5);
            System.out.println("executor2方法结束执行。。。");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "executor2";
    }
}

下面是controller层代码部分

@RestController
public class TestController {
    @Autowired
    private CartAsyncService asyncService;

    @GetMapping("test")
    public String test(){
        long start = System.currentTimeMillis();
        System.out.println("test方法----------开始执行-------------");
        asyncService.executor1();
        asyncService.executor2();
        System.out.println("test----------执行结束-------------耗时:" + (System.currentTimeMillis() - start)+"毫秒");
        return "hello cart!";
    }
}

启动springboot项目,浏览器访问localhost:8080/test测试发现响应浏览器仅需要1ms,这便是异步的威力了

如果方法executor1和executor2都不加@Async的话,浏览器至少等待9s才能响应,看得出来是同步的


3、返回Future

把方法的返回值用 AsyncResult 包装一下-----------------Future

@Service
public class CartAsyncService {
    @Async
    public Future<String> executor1() {
        try {
            System.out.println("executor1方法开始执行");
            //这个方法执行需要4秒
            TimeUnit.SECONDS.sleep(4);
            System.out.println("executor1方法结束执行。。。");
            return AsyncResult.forValue("executor1"); // 正常响应
        } catch (InterruptedException e) {
            e.printStackTrace();
            return AsyncResult.forExecutionException(e); // 异常响应
        }
    }

    @Async
    public Future<String> executor2() {
        try {
            System.out.println("executor2方法开始执行");
            //这个方法执行需要5秒
            TimeUnit.SECONDS.sleep(5);
            System.out.println("executor2方法结束执行。。。");
            return AsyncResult.forValue("executor2"); // 正常响应
        } catch (InterruptedException e) {
            return AsyncResult.forExecutionException(e); // 异常响应
        }
    }
}

修改controller中,使用Future接口处理

@RestController
public class TestController {
    @Autowired
    private CartAsyncService asyncService;

    @GetMapping("test")
    public String test() throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        System.out.println("test方法----------开始执行-------------");
        Future<String> f1= asyncService.executor1();
        System.out.println("Result 1: " + f1.get());
        Future<String> f2 = asyncService.executor2();
        System.out.println("Result 2: " + f2.get());
        System.out.println("test----------执行结束-------------耗时:" + (System.currentTimeMillis() - start)+"毫秒");
        return "hello cart!";
    }
}

因为get方法的阻塞,导致:每个方法按顺序执行,一个结果后再执行下一个。


4、返回ListenableFuture

org.springframework.util.concurrent.ListenableFuture;//回调处理结果和异常

SpringTask允许使用异步回调的方式,根据不同的响应结果做出不同的处理。SpringTask提供了ListenableFuture对象来实现自定义回调

把方法的返回值用 AsyncResult 包装一下-----------------ListenableFuture

@Service
public class CartAsyncService {
    @Async
    public ListenableFuture<String> executor1() {
        try {
            System.out.println("executor1方法开始执行");
            //这个方法执行需要4秒
            TimeUnit.SECONDS.sleep(4);
            System.out.println("executor1方法结束执行。。。");
            return AsyncResult.forValue("executor1"); // 正常响应
        } catch (InterruptedException e) {
            e.printStackTrace();
            return AsyncResult.forExecutionException(e); // 异常响应
        }
    }

    @Async
    public ListenableFuture<String> executor2() {
        try {
            System.out.println("executor2方法开始执行");
            //这个方法执行需要5秒
            TimeUnit.SECONDS.sleep(5);
            System.out.println("executor2方法结束执行。。。");
            return AsyncResult.forValue("executor2"); // 正常响应
        } catch (InterruptedException e) {
            return AsyncResult.forExecutionException(e); // 异常响应
        }
    }
}

修改controller中,使用回调处理

//spring-core源码
void addCallback(SuccessCallback<? super T> var1, FailureCallback var2);
  • 如果是正常的结果,调用 SuccessCallback 的回调。
  • 如果是异常的结果,调用 FailureCallback 的回调。
@RestController
public class TestController {
    @Autowired
    private CartAsyncService asyncService;

    @GetMapping("test")
    public String test() throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        System.out.println("test方法----------开始执行-------------");
        this.asyncService.executor1().addCallback(result -> {//异步任务成功的回调
            System.out.println("executor1的正常执行结果:" + result);
        }, ex -> {//异步任务失败的回调
            System.out.println("executor1执行出错:" + ex.getMessage());
        });

        this.asyncService.executor2().addCallback(result -> {//异步任务成功的回调
            System.out.println("executor2的正常执行结果:" + result);
        }, ex -> {//异步任务失败的回调
            System.out.println("executor2执行出错:" + ex.getMessage());
        });
        System.out.println("test----------执行结束-------------耗时:" + (System.currentTimeMillis() - start)+"毫秒");
        return "hello cart!";
    }
}

重新发起请求测试,控制台打印如下,接口调用时间为1ms,且俩个异步任务回调都没问题

-----------那什么时候会用到异常的回调

@Service
public class CartAsyncService {
    @Async
    public ListenableFuture<String> executor1() {
        try {
            System.out.println("executor1方法开始执行");
            //模拟数组角标越界异常
            int arr[] = new int[]{1, 2, 3};
            System.out.println(arr[3]);// 数组角标越界异常
            //这个方法执行需要4秒,
            TimeUnit.SECONDS.sleep(4);
            System.out.println("executor1方法结束执行。。。");
            return AsyncResult.forValue("executor1"); // 正常响应
        } catch (InterruptedException e) {
            e.printStackTrace();
            return AsyncResult.forExecutionException(e); // 异常响应
        }
    }

    @Async
    public ListenableFuture<String> executor2() {
        try {
            System.out.println("executor2方法开始执行");
            // 模拟文件未找到异常
            File file = new File("hello.txt");
            FileInputStream fileInputStream = new FileInputStream(file);
            //这个方法执行需要5秒
            TimeUnit.SECONDS.sleep(5);
            System.out.println("executor2方法结束执行。。。");
            return AsyncResult.forValue("executor2"); // 正常响应
        } catch (Exception e) {
            return AsyncResult.forExecutionException(e); // 异常响应
        }
    }
}

重新发起请求测试,控制台打印如下,


5、UncaughtExceptionHandler 

UncaughtExceptionHandler 是 Thread 类的一个内部接口:
当线程执行出现异常的时候,相当于会回调 UncaughtExceptionHandler 接口。

在 Thread 中有两种方式可以设置 UncaughtExceptio1nHandler

//设置全局 UncaughtExceptionHandler
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh);
//为当前 Thread 设置 UncaughtExceptionHandler
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh);

线程无法捕获它的派生线程的异常,测试如下:

public static void main(String[] args) {
    //线程无法捕获它的派生线程的异常
    try {
        new Thread(() -> {
            int i = 1 / 0;
        }).start();
    } catch (Throwable e) {
        System.out.println("error...");
    }
}

打印输出:

----------------------改造

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
        int i = 1 / 0;
    });
    thread.setUncaughtExceptionHandler((t, e) ->
        System.out.printf("线程【%s】发生异常,异常信息:\n%s", t.getName(), Arrays.toString(e.getStackTrace())));
    thread.start();
}

打印输出

UncaughtExceptionHandler 提供了一种回调处理异步线程执行失败的思想。

在 Spring 的 @Async 中可以通过配置 AsyncUncaughtExceptionHandler 在异步任务执行失败的时候进行处理。


6、全局异常处理器

返回值为ListenableFuture的异步方法可以使用异步回调处理异常结果,那么返回值为普通类型的异步方法出现异常该如何处理呢?

SpringTask提供了AsyncUncaughtExceptionHandler 接口,达到对异步调用异常的统一处理。

注意:AsyncUncaughtExceptionHandler 只能拦截返回类型非 Future 的异步调用方法。

返回类型为 Future 的异步调用方法,请使用异步回调来处理。

实现步骤

自定义全局异常处理器,实现AsyncUncaughtExceptionHandler 接口

/*
自定义异常处理器:
    只处理@Async标注方法的异常
 */
@Component
@Slf4j
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
        //打印日志
        log.error("方法:{} 执行出现异常:{} ,参数列表:{}",
                method.getName() , throwable.getMessage() , objects);
    }
}

再注册,添加配置类(@Configuration)实现AsyncConfigurer异步配置接口

@Configuration
public class AsyncConfig implements AsyncConfigurer {
//    @Override  给异步任务提供自定义线程池
//    public Executor getAsyncExecutor() {
//        return null;
//    }
    @Autowired
    AsyncExceptionHandler asyncExceptionHandler;
    //给异步任务提供异常处理器,出现异常时有该处理器处理
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return asyncExceptionHandler;
    }
}

Quartz

1、依赖引入与配置

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

使用第三方组件 Quartz


定时任务

定时任务技术

1.jdk自带的 timer(功能简单)
2.springTask提供的定时任务
3.quartz:经典的定时任务框架,但是配置繁琐且不能动态配置
4.xxl-job:分布式定时任务框架,基于quartz改进的可以动态配置

jdk自带的Timer

Java自带的java.util.Timer类,可以调度一个java.util.TimerTask任务。这种方式可以让程序按照某一个频度执行,但不能在指定的时间运行,一般用的比较少。

TimerTask是一个抽象类

public abstract class TimerTask implements Runnable{}

在重写的run()方法里去写具体要执行的任务

//记录日志的定时任务
public class LogTimerTask extends TimerTask {
    @Override
    public void run() {
        //定时任务执行的内容
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String strTime = sdf.format(new Date());
        System.out.println(strTime+":成功完成了一次数据备份!");
    }
}

java.util.Timer

这个Timer类里有一个很关键的方法,这里period单位是ms

//安排指定的任务(task),在指定的时间(firstTime)开始进行重复延迟(period)执行
schedule(TimerTask task,Date firstTime,long period)
public static void main(String[] args) throws ParseException {
    //创建定时器对象
    Timer timer = new Timer();
    //指定定时任务
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    Date firstTime = sdf.parse("2023-09-09 20:13:00");
    timer.schedule(new LogTimerTask(),firstTime,1000*2);
}

运行后,每个2s做一次日志记录备份。


springTask定时任务

Spring Boot 默认在无任何第三方依赖的情况下使用 spring-context 模块下提供的定时任务工具 Spring Task。我们只需要使用 @EnableScheduling 注解就可以开启相关的定时任务功能。

Spring Boot中使用,在程序入口启动类添加@EnableScheduling,开启定时任务功能

默认情况下 Spring Boot 定时任务是单线程执行的。当下一轮的任务满足时间策略后任务就会加入队列,也就是说当本次任务开始执行时下一次任务的时间就已经确定了,由于本次任务的“超时”执行,下一次任务的等待时间就会被压缩甚至阻塞,

定义定时任务逻辑,方法上加@Scheduled(cron="")

@Component
public class CartTask {
    //创建定时任务
    //从0秒开始  每过5秒执行一次
    @Scheduled(cron = "0/5 * * * * ?")
    public void test() {
        System.out.println(Thread.currentThread().getName() + " .. " + new Date());
    }
}

重新启动springboot项目后,控制台就会有效果了


fixedRate和fixedDelay

@Scheduled注解的源码

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {

    String cron() default "";

    String zone() default "";

    long fixedDelay() default -1;

    String fixedDelayString() default "";

    long fixedRate() default -1;

    String fixedRateString() default "";

    long initialDelay() default -1;

    String initialDelayString() default "";
}

可以看出,注解中可以传8种参数:

  • cron:指定cron表达式
  • zone:默认使用服务器默认时区。可以设置为java.util.TimeZone中的zoneId
  • fixedDelay:从上一个任务完成开始到下一个任务开始的间隔,单位毫秒
  • fixedDelayString:同上,时间值是String类型
  • fixedRate:从上一个任务开始到下一个任务开始的间隔,单位毫秒
  • fixedRateString:同上,时间值是String类型
  • initialDelay:任务首次执行延迟的时间,单位毫秒
  • initialDelayString:同上,时间值是String类型

fixedDelay理解

它的间隔时间是根据上次的任务结束的时候开始计时的。比如一个方法上设置了fixedDelay=5*1000,那么当该方法某一次执行结束后,开始计算时间,当时间达到5秒,就开始再次执行该方法。

@Component
public class CartTask {
    /**
     * 从启动时间开始,延迟 1s 后间隔 5s 执行
     * 固定等待时间
     */
    @Scheduled(fixedDelay = 5*1000, initialDelay = 1000)
    public void test() {
        System.out.println(Thread.currentThread().getName() + " .. " + new Date());
    }
}

initialDelay 初始化延迟时间,也就是第一次延迟执行的时间。

这个参数对 cron 属性无效,只能配合 fixedDelayfixedRate 使用。

@Scheduled(fixedDelay = 5*1000, initialDelay = 1000)表示第一次延迟 1000 毫秒执行,下一次任务在上一次任务结束后 5000 毫秒后执行。

fixedRate理解

它的间隔时间是根据上次任务开始的时候计时的。比如当方法上设置了fiexdRate=5*1000,该执行该方法所花的时间是2秒,那么3秒后就会再次执行该方法。

@Component
public class CartTask {
    /**
     * 从启动时间开始,间隔 5s 执行
     * 固定间隔时间
     */
    @Scheduled(fixedRate = 5*1000)
    public void test() {
        System.out.println(Thread.currentThread().getName() + " .. " + new Date());
    }
}

当任务执行时长超过设置的间隔时长,比如因为网络问题导致这个任务花了7秒才执行完成。当任务开始时Spring就会给这个任务计时,5秒钟时候Spring就会再次调用这个任务,可是发现原来的任务还在执行,这个时候第二个任务就阻塞了。甚至如果第一个任务花费的时间过长,还可能会使第三第四个任务被阻塞。被阻塞的任务就像排队的人一样,一旦前一个任务没了,它就立马执行。


异步的定时任务调度

答案是加上注解@EnableAsync(类上)和@Async(方法上),加了注解以后,就开启了多线程模式,当到了下一次任务的执行时机时,如果上一次任务还没执行完,就会自动创建一个新的线程来执行它。异步执行也可以理解为保证了任务以固定速度执行。

同时开启同步和异步任务,假设任务本身耗时较长,且间隔较短:间隔1s,执行10s,同步与异步执行的差异就此体现。


Cron表达式

Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义

并表示:秒  分  时  日  月 周  年 

周和日通配所有时必须有一个是?,不能同时使用*

1.具体有效值:所有字段都有一组可以指定的有效值

秒: 有效范围为0-59的整数

分: 有效范围为0-59的整数

小时:有效范围为0-23的整数

日期:有效范围为0-31的整数,但是您需要注意在给定的月份中有多少天!

月份:可以指定为1到12之间的值,或者JAN-DEC

JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV和DEC。

星期几  有效范围为1-7的整数或SUN-SAT两个范围。1表示星期天,2表示星期一.

SUN,MON,TUE,WED,THU,FRI和SAT。

: 有效范围为1970-2099年

2.单个域可以包含范围(-)或列表(,)

例如,可以用“MON-FRI”,“MON,WED,FRI”或甚至“MON-WED,SAT”代替如下示例中的星期几字段。

“0 0 12 ?* WED“
每个星期三下午12:00

3. * 通配符可用于说明该字段的“每个”可能的值。

因此,前一个例子的“月”字段中的“”字符仅仅是“每个月”。因此,“星期几”字段中的“*”显然意味着“每周的每一天”。

4.  /字符 可用于指定值的增量。

例如,如果在“分钟”字段中输入“0/15”,则表示“从零开始,每隔15分钟处罚一次”。

如果您在“分钟”字段中使用“3/20”,则意味着“从三分钟开始,每隔20分钟触发一次” - 换句话说,它与“分钟”中的“3,23,43”相同领域。

请注意“ /35”的细微之处并不代表“每35分钟” - 这意味着“从零开始,每隔35分钟触发一次” - 或者换句话说,与指定“0,35”相同。

5. ?字符 是允许的日期和星期几字段

用于指定“无特定值”。只有在其中一个字段指定了值,另一个字段不指定时才能使用。这两个字段不能同时为*或者?

6. L字符 允许用于月日和星期几字段,相当于最后。

例如,“月”字段中的“L”表示“月的最后一天” - 1月31日,非闰年2月28日。如果在本周的某一天使用,它只是意味着“7”或“SAT”。但是如果在星期几的领域中再次使用这个值,就意味着“一个月的最后一个xxx日”,例如“6L”或“FRIL”都意味着“月的最后一个星期五”。您还可以指定从该月最后一天的偏移量,例如“L-3”,这意味着日历月份的第三个到最后一天。当使用'L'选项时,重要的是不要指定列表或值的范围,因为您会得到混乱/意外的结果。

7.W用于指定最近给定日期的工作日(星期一至星期五)。

例如,如果要将“15W”指定为月日期字段的值,则意思是:“最近的平日到当月15日”。

8.用于指定本月的“第n个”XXX工作日。

例如,“星期几”字段中的“6#3”或“FRI#3”的值表示“本月的第三个星期五”。


分布式定时任务xxl-Job

XXL-JOB支持通过 Web 页面对任务进行 CRUD 操作,支持动态修改任务状态、暂停/恢复任务,以及终止运行中任务,支持在线配置调度任务入参和在线查看调度结果。

源码参照github:GitHub - xuxueli/xxl-job: A distributed task scheduling framework.(分布式任务调度平台XXL-JOB)

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

项目结构

解析下载的源码压缩包,项目结构分析

xxl-job-admin:调度中心

xxl-job-core:公共依赖

xxl-job-executor-samples:执行器Sample示例(选择合适的版本执行器,可直接使用,也可以参考其并将现有项目改造成执行器) :

        xxl-job-executor-sample-springboot:Springboot版本管理执行器推荐这种方式 

调度中心:统一管理任务调度平台上的调度任务,负责触发调度执行,并且提供任务管理平台。

执行器:负责接收“调度中心”的调度并执行;可直接部署执行器,也可以将执行器集成到现有业务项目中

XXL-JOB中“调度模块”和“任务模块”完全解耦,调度模块进行任务调度时,将会解析不同的任务参数发起远程调用,调用各自的远程执行器服务。这种调用模型类似RPC调用,调度中心提供调用代理的功能,而执行器提供远程服务的功能。


部署调度中心

1、初始化“调度数据库”

“调度数据库初始化SQL脚本” 位置为:/xxl-job/doc/db/tables_xxl_job.sql

完成后数据库效果如下:

表说明:

- xxl_job_lock:任务调度锁表。
- xxl_job_group:执行器信息表,维护任务执行器信息。
- xxl_job_info:调度扩展信息表: 用于保存XXL-JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等。
- xxl_job_log:调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等。
- xxl_job_log_report:调度日志报表:用户存储XXL-JOB任务调度日志的报表,调度中心报表功能页面会用到。
- xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能。
- xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息。
- xxl_job_user:系统用户表。


2、修改调度中心配置

调度中心配置文件:application.properties

主要关注以下3部分配置:端口号、jdbc数据源、报警邮箱、xxl调度中心配置

### 端口号
server.port=8080

### 调度中心JDBC链接:链接地址请保持和 调度数据库的地址一致
spring.datasource.url=jdbc:mysql://172.16.116.100:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

### 报警邮箱
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=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

### 调度中心通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=

### 调度中心国际化配置 [必填]: 默认为 "zh_CN"/中文简体, 可选范围为 "zh_CN"/中文简体, "zh_TC"/中文繁体 and "en"/英文
xxl.job.i18n=zh_CN

## 调度线程池最大线程配置【必填】
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100

### 调度中心日志表数据保存天数 [必填]:过期日志自动清理;限制大于等于7时生效,否则, 如-1,关闭自动清理功能;
xxl.job.logretentiondays=30

3、启动调度中心

在浏览器访问: 任务调度中心

默认登录账号 “admin/123456”, 登录后运行界面如下图所示。

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


搭建执行器项目

执行器负责接收“调度中心”的调度并执行;在源码中作者已经贴心的给出了多种执行器项目示例(官方推荐的xxl-job-executor-sample-springboot项目为例部署),可根据你的喜好直接将其部署作为你自己的执行器,也可以将执行器集成到现有业务项目中去。

这里以集成到现有项目为例,将执行器集成到现有的项目中去

1、添加xxl-job-core依赖

<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>2.2.0</version>
</dependency>

2、修改执行器配置文件

配置内容如下:

# 端口号
server.port=8081
# no web
#spring.main.web-environment=false
# log config
#logging.config=classpath:logback.xml

### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。
### 执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin

### 执行器通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=

### 执行器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=
### 执行器端口号[选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
xxl.job.executor.port=9999
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### 执行器日志文件保存天数[选填]:过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
xxl.job.executor.logretentiondays=30

3、添加执行器配置类

不解释,直接拷贝。

@Configuration
public class XxlJobConfig {
    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

    @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() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        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;
    }

    /**
     * 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP;
     *
     *      1、引入依赖:
     *          <dependency>
     *             <groupId>org.springframework.cloud</groupId>
     *             <artifactId>spring-cloud-commons</artifactId>
     *             <version>${version}</version>
     *         </dependency>
     *
     *      2、配置文件,或者容器启动变量
     *          spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
     *
     *      3、获取IP
     *          String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
     */
}

4、给执行器添加任务

@Component
public class MyJobHandler {
    private static Logger logger = LoggerFactory.getLogger(MyJobHandler.class);

    @XxlJob("myJobHandler") // 注解中的值表示该任务注册到调度中心的任务名称
    public ReturnT<String> demoJobHandler(String param) {
        logger.info("param: "+ param);
        //通过 "XxlJobLogger.log()" 打印执行日志;
        XxlJobLogger.log("XXL-JOB, Hello World. : "+ param);
        return ReturnT.SUCCESS;
    }
}

5、执行器管理界面-添加执行器

点击进入”执行器管理”界面, 如下图:

"执行器列表" 中显示在线的执行器列表, 可通过"OnLine 机器"查看对应执行器的集群机器

点击新增按钮,把执行器项目添加进来

新增/编辑”执行器管理“界面:

  • AppName: 是每个执行器集群的唯一标示AppName, 执行器会周期性以AppName为对象进行自动注册。可通过该配置自动发现注册成功的执行器, 供任务调度时使用。

  • 名称: 执行器的名称,因为AppName限制字母数字等组成,可读性不强,名称为了提高执行器的可读性。

  • 注册方式:调度中心获取执行器地址的方式。

    • 自动注册:执行器自动进行执行器注册,调度中心通过底层注册表可以动态发现执行器机器地址

    • 手动录入:人工手动录入执行器的地址信息,多地址逗号分隔,供调度中心使用;

  • 机器地址:"注册方式"为"手动录入"时有效,支持人工维护执行器的地址信息。

添加完成后效果如下:


6、配置执行器任务到调度中心

去调度中心配置新增执行器任务

登录调度中心,点击下图所示“新建”按钮

参考下面截图中任务的参数配置,点击保存。

成功后如下

执行一次,后查询日志

 在idea的控制台也是打印了

你也可以点击任务右侧的”启动“按钮执行多次,真正的启动任务

此时任务状态是绿色”RUNNING“:

 再去查看调度日志,可以看到很多执行日志的记录:

 在idea控制台中


7、任务界面细说

  • 执行器:任务绑定的执行器,任务触发调度时将会自动发现注册成功的执行器, 实现任务自动发现功能;另一方面也可以方便的进行任务分组。每个任务必须绑定一个执行器, 可在 "执行器管理" 进行设置。

  • 任务描述:任务的描述信息,便于任务管理。

  • 路由策略:当执行器集群部署时,提供丰富的路由策略,包括:

    • FIRST / LAST:固定选择第一个/最后一个机器。

    • ROUND(轮询)/ RANDOM(随机)

    • CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。

    • LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举。

    • LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举。

    • FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度。

    • BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度。

    • SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务。

  • Cron:触发任务执行的Cron表达式。

  • 运行模式:

    • BEAN模式:任务以JobHandler方式维护在执行器端;需要结合 "JobHandler" 属性匹配执行器中任务。

    • GLUE模式(Java):任务以源码方式维护在调度中心;该模式的任务实际上是一段继承自IJobHandler的Java类代码并 "groovy" 源码方式维护,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务。

    • GLUE模式(Shell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "shell" 脚本; GLUE模式(Python):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "python" 脚本; GLUE模式(PHP):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "php" 脚本; GLUE模式(NodeJS):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "nodejs" 脚本; GLUE模式(PowerShell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "PowerShell" 脚本。

  • JobHandler:运行模式为 "BEAN模式" 时生效,对应执行器中新开发的JobHandler类“@JobHandler”注解中自定义的value值。

  • 阻塞处理策略:调度过于密集执行器来不及处理时的处理策略。

    • 单机串行(默认):调度请求进入单机执行器后,调度请求进入FIFO队列并以串行方式运行。

    • 丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败。

    • 覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务。

  • 子任务:每个任务都拥有一个唯一的任务ID(任务ID可以从任务列表获取),当本任务执行结束并且执行成功时,将会触发子任务ID所对应的任务的一次主动调度。

  • 任务超时时间:支持自定义任务超时时间,任务运行超时将会主动中断任务。

  • 失败重试次数;支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试。

  • 报警邮件:任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔。

  • 负责人:任务的负责人。

  • 执行参数:任务执行所需的参数。

根据引用\[1\]中的信息,xxl-job是一个分布式定时任务框架,可以解决一些传统定时任务框架的问题。然而,根据引用\[3\]中的描述,xxl-jobcron时间可能导致不能新增定时任务。具体来说,xxl-jobcron时间是指定任务执行的时间表达式,如果设置不正确,可能会导致任务无法正常新增。 要解决这个问题,可以检查cron时间表达式是否正确。可以参考引用\[2\]中的代码示例,使用DateUtils.formatDate方法来格式化cron时间,确保它的格式是正确的。另外,还可以参考xxl-job的官方文档或者寻求相关技术支持,以获取更详细的解决方案。 总结起来,如果xxl-jobcron时间导致不能新增定时任务,可以检查cron时间表达式是否正确,并参考官方文档或寻求技术支持来解决问题。 #### 引用[.reference_title] - *1* [xxl-job定时任务](https://blog.csdn.net/m0_49790240/article/details/122717201)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [分布式定时任务xxljob](https://blog.csdn.net/weixin_45613222/article/details/129222029)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [22-09-29 西安 谷粒商城07分布式定时任务xxl-jobCron表达式springTask定时任务、订单业务](https://blog.csdn.net/m0_56799642/article/details/127109242)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值