RuoYi-Vue-Plus (XXL-JOB任务调度中心二:配置管理与定时任务编写、执行策略、命令行任务、邮件报警等等

一、后端xxl job的配置属性介绍

  • enabled : 是否开启执行器,如果为false,调度中心就调用不了后端定时任务

  • admin-addresses:调度中心的地址,多个则可以逗号拼接: url1,url2,url3

  • access-token: 执行器通讯TOKEN ,必须和xxl-job代码里面的一致

    java 复制代码
    src/main/resources/application.yml 配置文件中 accessToken:
    
    --- # xxljob系统配置
    xxl:
      job:
        # 鉴权token
        accessToken: xxl-job
        # 国际化
        i18n: zh_CN
        # 日志清理
        logretentiondays: 30
        triggerpool:
          fast:
            max: 200
          slow:
            max: 100
  • appname: 执行器AppName, 配置完调度中心可以识别到

复制代码
port: 执行器端口号,执行器名称可以一致appname: xxl-job-executor,但是如果多个执行器端口号必须区分开来
  • address: 执行器注册地址,默认自动获取IP
  • ip: 执行器IP:默认自动获取IP
  • **logpath:**日志保存路径
复制代码
logretentiondays: 日志保存天数

通过配置可以知道,xxl-job 调度中心识别执行器的流程:

开启执行器后,执行器 根据admin-addresses 地址和 xxl-job 注册到 调度中心,并绑定执行器名称端口日志等信息

java 复制代码
--- # xxl-job 配置
xxl.job:
  # 执行器开关
  enabled: true
  #enabled: false
  # 调度中心地址:如调度中心集群部署存在多个地址则用逗号分隔。
  admin-addresses: http://localhost:9100/xxl-job-admin
  # 执行器通讯TOKEN:非空时启用
  access-token: xxl-job
  executor:
    # 执行器AppName:执行器心跳注册分组依据;为空则关闭自动注册
    appname: xxl-job-executor
    # 28080 端口 随着主应用端口飘逸 避免集群冲突
    port: 2${server.port}
    # 执行器注册:默认IP:PORT
    address:
    # 执行器IP:默认自动获取IP
    ip:
    # 执行器运行日志文件存储磁盘路径
    logpath: ./logs/xxl-job
    # 执行器日志文件保存天数:大于3生效
    logretentiondays: 30

测试:启动调度中心和后台,将后台注册到调度中心

启动xxx-job调度中心和 后台执行器,

则可以看到注册成功:

二、任务管理

2.1 常用配置:

  1. 调度配置-调度类型: 一般我们默认选择CRON表达式,可以参考文章:cron 定时任务_cron 每月最后一天-CSDN博客

  2. **任务配置-运行模式:**默认BEAN模式,后面代码会演示 分片广播、命令行任务

  3. 任务配置-JobHandler: 对应注解里面标注的名称 @XxlJob("demoJobHandler")

    java 复制代码
      /**
         * 1、简单任务示例(Bean模式)
         */
        @XxlJob("demoJobHandler")
        public void demoJobHandler() throws Exception {
            XxlJobHelper.log("XXL-JOB, Hello World.");
    
            for (int i = 0; i < 5; i++) {
                XxlJobHelper.log("beat at:" + i);
            }
            // default success
        }
  4. 高级配置-路由策略:

    XML 复制代码
    第一个(First):总是选择注册列表中的第一个执行器执行。
    
    最后一个(Last):总是选择注册列表中的最后一个执行器执行。
    
    轮询(Round):按顺序轮流选择执行器执行,实现简单负载均衡。
    
    随机(Random):随机选择一个执行器执行,增加执行的不确定性。
    
    一致性HASH(Consistent Hash):通过Hash算法稳定地选择执行器,即使执行器数量变化也能保持任务分配的连续性。
    
    最少使用(Least Used)(LFU或类似概念):选择最近最少被使用的执行器,以平衡负载。
    
    最近未使用(Least Recently Used, LRU):选择最久未被使用的执行器执行。
    
    故障转移(Failover):当首选执行器不可用时,自动选择下一个可用的执行器。
    
    忙碌转移(Busyover):不仅考虑故障,还考虑执行器的忙碌状态,避免过载。
    
    分片广播(Sharding Broadcast):所有执行器都执行一次任务,但每个执行器可以处理不同的数据分片,适用于大数据量处理。

高级配置-阻塞处理策略:

XML 复制代码
单机串行(默认):
描述:任务按调度顺序一个接一个地执行,前一个执行完再执行下一个。
特点:简单,但可能导致任务等待时间长。

丢弃后续调度:
描述:如果执行器正忙,新任务会被丢弃。
特点:防止任务堆积,但可能会丢失任务。

覆盖之前调度:
描述:如果执行器正忙,会中断当前任务,立即执行新任务。
特点:确保新任务快速执行,但可能影响旧任务。
  1. 高级配置-任务超时时间 : 默认0, 表示不超时
  2. 高级配置-失败重试次数 : 默认0, 表示不重试

三、 ruoyi-job 模块

ruoyi-job 是我们定义统一编写定时任务的子模块,现在看看怎么编写第一个定时任务:

引入依赖xxl-job核心包、已经公共common包

XML 复制代码
 <dependencies>

        <!-- 通用工具-->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-common</artifactId>
        </dependency>

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

    </dependencies>

1- 编写 XxlJobProperties

@ConfigurationProperties(prefix = "xxl.job") 映射 yml中的配置属性

java 复制代码
@Data
@ConfigurationProperties(prefix = "xxl.job")
public class XxlJobProperties {

    private Boolean enabled;

    private String adminAddresses;

    private String accessToken;

    private Executor executor;

    @Data
    @NoArgsConstructor
    public static class Executor {

        private String appname;

        private String address;

        private String ip;

        private int port;

        private String logPath;

        private int logRetentionDays;
    }
}

2- 编写 XxlJobConfig 配置类

主要就是启动执行器,并配置调度中心地址、token、执行器等等

注解解释:

@ConditionalOnProperty(prefix = "xxl.job", name = "enabled", havingValue = "true") :

开启配置属性:xxl.job 中 enabled为true时候才会 注册, @Bean注解标注的xxlJobExecutor 类

java 复制代码
@Slf4j
@Configuration //标注配置类
@EnableConfigurationProperties(XxlJobProperties.class) //启用配置属性
@AllArgsConstructor
@ConditionalOnProperty(prefix = "xxl.job", name = "enabled", havingValue = "true") //开启配置属性:enabled为true时候才会 注册:xxlJobExecutor
public class XxlJobConfig {

    private final XxlJobProperties xxlJobProperties;

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
         log.info(">>>>>>>>>>> xxl-job config init.");
        // 初始化 xxl-job 执行器
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        // 配置 调度中心地址
        xxlJobSpringExecutor.setAdminAddresses(xxlJobProperties.getAdminAddresses());
        // 配置 token
        xxlJobSpringExecutor.setAccessToken(xxlJobProperties.getAccessToken());
        // 配置 执行器
        XxlJobProperties.Executor executor = xxlJobProperties.getExecutor();
        //执行器相关属性
        xxlJobSpringExecutor.setAppname(executor.getAppname());
        xxlJobSpringExecutor.setAddress(executor.getAddress());
        xxlJobSpringExecutor.setIp(executor.getIp());
        xxlJobSpringExecutor.setPort(executor.getPort());
        xxlJobSpringExecutor.setLogPath(executor.getLogPath());
        xxlJobSpringExecutor.setLogRetentionDays(executor.getLogRetentionDays());
        return xxlJobSpringExecutor;
    }

}

3-编写定时任务

完成上面配置类,便可以直接编写定时任务

java 复制代码
@Slf4j
@Service
public class SampleService {

    /**
     * 1、简单任务示例(Bean模式)
     */
    @XxlJob("demoJobHandler")
    public void demoJobHandler() throws Exception {
        XxlJobHelper.log("XXL-JOB, Hello World.");

        for (int i = 0; i < 5; i++) {
            XxlJobHelper.log("beat at:" + i);
        }
        // default success
    }

四、参数传递

更新任务时候设置参数,如图:

运行任务后台便可以接受到参数:

java 复制代码
  @XxlJob("demoJobHandler")
    public void demoJobHandler() throws Exception {
        XxlJobHelper.log("XXL-JOB, Hello World.");

        String jobParam = XxlJobHelper.getJobParam();
        log.info("jobParam:{}" , jobParam);


        for (int i = 0; i < 5; i++) {
            XxlJobHelper.log("beat at:" + i);
        }
        // default success
    }

结果:

java 复制代码
2024-08-04 15:39:32 JRebel: Reconfiguring reprocessed bean 'sampleService' [com.ruoyi.job.service.SampleService]
2024-08-04 15:39:36 [xxl-job, JobThread-1-1722757089189] INFO  com.ruoyi.job.service.SampleService
 - jobParam:type=1&flag=true

当然,JSON也可以传递都后台,按需求进行处理即可

java 复制代码
2024-08-04 15:43:09 [xxl-job, EmbedServer bizThreadPool-1028558242] INFO  c.x.job.core.executor.XxlJobExecutor
 - >>>>>>>>>>> xxl-job regist JobThread success, jobId:1, handler:com.xxl.job.core.handler.impl.MethodJobHandler@6146a40f[class com.ruoyi.job.service.SampleService#demoJobHandler]
2024-08-04 15:43:09 [xxl-job, JobThread-1-1722757389734] INFO  com.ruoyi.job.service.SampleService
 - jobParam:{"type":1,&"code":0}

五、轮询策略

前置场景,启动2个端口不一样的执行器8080和8081端口,则会在2个注册的节点上轮流访问

总结:

  1. 执行器交替执行,减少服务器压力,实现负载均衡
  2. 如果一个节点挂了则会交给另一个节点处理

六、分片广播

场景:如果我们要同时通知1万个对象,当前有5个执行器采用轮询则要循环1万次 ,所以此场景就应该采用分片广播策略

新增分片广播任务

后台处理:

对分片总是进行取余模拟,分片执行

java 复制代码
 /**
     * 2、分片广播任务
     */
    @XxlJob("shardingJobHandler")
    public void shardingJobHandler() throws Exception {

        // 分片索引
        int shardIndex = XxlJobHelper.getShardIndex();
        // 分片总数
        int shardTotal = XxlJobHelper.getShardTotal();


        //循环10次,模拟分片广播
        for (int i = 0; i < 10; i++) {
            //取余模拟,分片执行
            if (i % shardTotal == shardIndex) {
                XxlJobHelper.log("第 {} 片, 命中分片开始处理", i);
            }
        }

    }

执行结果:
执行新增分片广播任务,生成2条调用日志

分别查看日志内容:

分片0执行了,第 0、4、6、8任务

分片1执行了,第 1、3、5、7、9任务

这样对比轮询去处理该场景任务,极大的提升了任务处理的速度

分片广播的应用场景

  • 数据处理任务:如对大量数据进行清洗、分析、转换等操作,可以将任务拆分成多个小任务,分布式地执行。

  • 分布式计算任务:如对大规模数据进行机器学习、深度学习等计算,可以将计算任务拆分成多个小任务,分布式地执行,加速计算过程。

  • 并发请求任务:如对多个服务进行并发请求,可以将请求拆分成多个小请求,分布式地执行,提高请求的并发处理能力。
    注意事项:

  • 在实现分片广播任务时,需要确保执行器集群中的执行器回调地址(xxl.job.admin.addresses)保持一致。

  • 同一个执行器集群内的AppName(xxl.job.executor.appname)也需要保持一致。

  • 在任务执行过程中,需要考虑并发执行的情况,确保任务逻辑的正确性。

总之,XXL-JOB的分片广播功能为分布式任务调度提供了一种高效、灵活的实现方式,适用于多种需要并行处理的任务场景。

七、命令行任务

演示window下执行命令行,linux也是一样的

1-先新记事本建文件 mkdir.bat文件,内容如下:

命令行执行命令:新建一个文件夹,yyyy-MM-dd HH-mm-ss 作为文件夹名称

java 复制代码
@echo off
setlocal enabledelayedexpansion

:: 获取当前日期
for /f "tokens=2-4 delims=/-" %%a in ('date /t') do (
    set YY=%%c
    set MM=%%a
    set DD=%%b
)

:: 获取当前时间
for /f "tokens=1-3 delims=: " %%h in ('time /t') do (
    set HH=%%h
    set MM=%%i
    set SS=%%j
)

:: 格式化日期和时间字符串以适应文件夹名
set "folderName=%YY%-%MM%-%DD%_%HH%-%MM%-%SS%"
:: 创建文件夹
md "D:\CPanFiles\download\!folderName!" 

2-创建调度任务:

任务运行参数为上面mkdir.bat文件的地址,我放在D盘downlad文件夹下面

复制代码
3-commandJobHandler 运行命令行后台如下:

获取定时任务参数,执行命令行

java 复制代码
    @XxlJob("commandJobHandler")
    public void commandJobHandler() throws Exception {
        //1-获取参数
        String command = XxlJobHelper.getJobParam();
        int exitValue = -1;

        BufferedReader bufferedReader = null;
        try {
            // command
            ProcessBuilder processBuilder = new ProcessBuilder();
            processBuilder.command(command);
            processBuilder.redirectErrorStream(true);

            // 执行命令行
            Process process = processBuilder.start();
            //Process process = Runtime.getRuntime().exec(command);

            //字符流 读取
            BufferedInputStream bufferedInputStream = new BufferedInputStream(process.getInputStream());
            bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream));

            // 记录到日志表
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                XxlJobHelper.log(line);
            }

            process.waitFor();
            exitValue = process.exitValue();
        } catch (Exception e) {
            XxlJobHelper.log(e);
        } finally {
            if (bufferedReader != null) {
                bufferedReader.close();
            }
        }

        if (exitValue == 0) {
            // default success
        } else {
            XxlJobHelper.handleFail("command exit value(" + exitValue + ") is failed");
        }

    }

前台点击执行任务:生成文件夹成功

八、HTTP平台请求

这里模拟发送请求到百度

复制代码
httpJobHandler 后台:

拿到参数,发送HTTP请求,返回请求结果

java 复制代码
    @XxlJob("httpJobHandler")
    public void httpJobHandler() throws Exception {

        // param parse
        String param = XxlJobHelper.getJobParam();
        if (param == null || param.trim().length() == 0) {
            XxlJobHelper.log("param[" + param + "] invalid.");

            XxlJobHelper.handleFail();
            return;
        }

        //获取http请求参数
        String[] httpParams = param.split("\n");
        String url = null;
        String method = null;
        String data = null;
        for (String httpParam : httpParams) {
            if (httpParam.startsWith("url:")) {
                url = httpParam.substring(httpParam.indexOf("url:") + 4).trim();
            }
            if (httpParam.startsWith("method:")) {
                method = httpParam.substring(httpParam.indexOf("method:") + 7).trim().toUpperCase();
            }
            if (httpParam.startsWith("data:")) {
                data = httpParam.substring(httpParam.indexOf("data:") + 5).trim();
            }
        }

        //校验参数
        if (url == null || url.trim().length() == 0) {
            XxlJobHelper.log("url[" + url + "] invalid.");

            XxlJobHelper.handleFail();
            return;
        }
        if (method == null || !Arrays.asList("GET", "POST").contains(method)) {
            XxlJobHelper.log("method[" + method + "] invalid.");

            XxlJobHelper.handleFail();
            return;
        }
        boolean isPostMethod = method.equals("POST");

        // request 发送http请求
        HttpURLConnection connection = null;
        BufferedReader bufferedReader = null;
        try {
            // connection
            URL realUrl = new URL(url);
            connection = (HttpURLConnection) realUrl.openConnection();

            // connection setting
            connection.setRequestMethod(method);
            connection.setDoOutput(isPostMethod);
            connection.setDoInput(true);
            connection.setUseCaches(false);
            connection.setReadTimeout(5 * 1000);
            connection.setConnectTimeout(3 * 1000);
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
            connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8");

            // do connection
            connection.connect();

            // data
            if (isPostMethod && data != null && data.trim().length() > 0) {
                DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream());
                dataOutputStream.write(data.getBytes("UTF-8"));
                dataOutputStream.flush();
                dataOutputStream.close();
            }

            // valid StatusCode :获取http状态
            int statusCode = connection.getResponseCode();
            if (statusCode != 200) {
                throw new RuntimeException("Http Request StatusCode(" + statusCode + ") Invalid.");
            }

            // result :获取返回结果
            bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
            StringBuilder result = new StringBuilder();
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                result.append(line);
            }
            String responseMsg = result.toString();

            XxlJobHelper.log(responseMsg);

            return;
        } catch (Exception e) {
            XxlJobHelper.log(e);

            XxlJobHelper.handleFail();
            return;
        } finally {
            try {
                if (bufferedReader != null) {
                    bufferedReader.close();
                }
                if (connection != null) {
                    connection.disconnect();
                }
            } catch (Exception e2) {
                XxlJobHelper.log(e2);
            }
        }

    }

执行结果:跳转到百度登录页面:

9-生命周期

@XxlJob(value = "demoJobHandler2", init = "init", destroy = "destroy")

init :当 XXL-JOB 框架启动并加载此任务时,会尝试调用标注了 @XxlJob 注解的类中的 init 方法(如果存在), 这可以用于执行一些初始化操作,比如资源加载、数据库连接初始化等

destroy 当 XXL-JOB 框架停止或卸载此任务时,会尝试调用标注了 @XxlJob 注解的类中的 destroy 方法(如果存在),这可以用于执行一些清理操作,比如关闭数据库连接、释放资源等。

java 复制代码
  @XxlJob(value = "demoJobHandler2", init = "init", destroy = "destroy")
    public void demoJobHandler2() throws Exception {
        XxlJobHelper.log("XXL-JOB, Hello World.");
    }

    public void init() {
        log.info("init");
    }

    public void destroy() {
        log.info("destory");
    }

十、报警邮件配置

1-以qq邮箱举例接受错误报警提醒进入到安全设置(页面右上角)

2-生成授权码:

3-后台配置下 授权码 (上面生成的 ) 以及 邮箱账号

java 复制代码
--- # 邮件配置
spring:
  mail:
    from: 13361831qq.com
    host: smtp.qq.com
    username: 13361831qq.com
    # 邮箱授权码
    password: ixxxj0000jfoghbih
    port: 25
    properties:
      mail:
        smtp:
          auth: true
          socketFactory:
            class: javax.net.ssl.SSLSocketFactory
          starttls:
            enable: true
            required: true

4- 创建定时任务绑定邮箱

执行任务结果:收到信息成功

相关推荐
激流丶11 分钟前
【Kafka 实战】如何解决Kafka Topic数量过多带来的性能问题?
java·大数据·kafka·topic
Themberfue14 分钟前
Java多线程详解⑤(全程干货!!!)线程安全问题 || 锁 || synchronized
java·开发语言·线程·多线程·synchronized·
让学习成为一种生活方式31 分钟前
R包下载太慢安装中止的解决策略-R语言003
java·数据库·r语言
晨曦_子画37 分钟前
编程语言之战:AI 之后的 Kotlin 与 Java
android·java·开发语言·人工智能·kotlin
南宫生1 小时前
贪心算法习题其三【力扣】【算法学习day.20】
java·数据结构·学习·算法·leetcode·贪心算法
Heavydrink1 小时前
HTTP动词与状态码
java
ktkiko111 小时前
Java中的远程方法调用——RPC详解
java·开发语言·rpc
计算机-秋大田2 小时前
基于Spring Boot的船舶监造系统的设计与实现,LW+源码+讲解
java·论文阅读·spring boot·后端·vue
神里大人2 小时前
idea、pycharm等软件的文件名红色怎么变绿色
java·pycharm·intellij-idea
小冉在学习2 小时前
day53 图论章节刷题Part05(并查集理论基础、寻找存在的路径)
java·算法·图论