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- 创建定时任务绑定邮箱

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

相关推荐
TDengine (老段)5 分钟前
TDengine 数学函数 CRC32 用户手册
java·大数据·数据库·sql·时序数据库·tdengine·1024程序员节
心随雨下24 分钟前
Tomcat日志配置与优化指南
java·服务器·tomcat
Kapaseker30 分钟前
Java 25 中值得关注的新特性
java
wljt34 分钟前
Linux 常用命令速查手册(Java开发版)
java·linux·python
撩得Android一次心动37 分钟前
Android 四大组件——BroadcastReceiver(广播)
android·java·android 四大组件
canonical_entropy40 分钟前
Nop平台到底有什么独特之处,它能用在什么场景?
java·后端·领域驱动设计
chilavert31843 分钟前
技术演进中的开发沉思-174 java-EJB:分布式通信
java·分布式
不是株1 小时前
JavaWeb(后端进阶)
java·开发语言·后端
编程火箭车1 小时前
【Java SE 基础学习打卡】02 计算机硬件与软件
java·电脑选购·计算机基础·编程入门·计算机硬件·软件系统·编程学习路线