Spring@Scheduled定时任务接入XXL-JOB的一种方案(基于SC Gateway)

news2024/12/23 8:22:04

背景

目前在职的公司,维护着Spring Cloud分布式微服务项目有25+个。其中有10个左右微服务都写有定时任务逻辑,采用Spring @Scheduled这种方式。

Spring @Scheduled定时任务的缺点:

  1. 不支持集群:为避免重复执行,需引入分布式锁
  2. 死板不灵活:不支持手动执行,单次执行,补偿执行,修改任务参数,暂停任务,删除任务,修改调度时间,失败重试
  3. 无报警机制:任务失败之后没有报警机制,逻辑执行异常记录ERROR日志接入Prometheus告警这种方式不算,这算是日志层面的告警,而不是任务层面的告警机制
  4. 不支持分片任务:处理有序数据时,多机器分片执行任务处理不同数据
  5. ……

基于此,考虑引入轻量级分布式定时调度框架XXL-JOB,即把定时任务迁到XXL-JOB平台。

关于XXL-JOB,可参考之前的blog。

设计方案

考虑到我们有10+个SC分布式应用,30+个定时任务。如果每个应用都需要迁移改造的话,则每个应用都需要配置XXL-JOB相关的信息。当然,这可以通过Apollo namespace共享继承机制来实现。题外话:有空的话,后面会写一篇Apollo namespace配置继承的blog。

也就是说,我可以在一个应用里(一个应用对应着一个Apollo namespace)的Apollo里维护好XXL-JOB的配置信息,其他应用通过复用此应用(的Apollo)来实现配置复用。

但是每个应用还得新增一个配置类,配置类怎么实现复用呢?这也能解决。解决方案就是在commons组件库里维护配置类(需要引入Spring @Configuration注解,即引入spring-context依赖包),然后每个应用的Spring Boot启动类里需要扫描到此配置类。

还得改造一下30+个定时任务对应的30+个@@Component定时任务类,所有的定时任务应用都需要引入maven依赖。

还得手动在XXL-JOB里新增定时任务类。

看起来还不错的方案,但是不排除不同的应用有同名的配置,遇到同名的配置,则需要修改配置命名。Spring Boot启动类改造可能会带来未知的问题。

最后的最后,考虑到我们所有的应用都需要经过Gateway网关服务来转发,不管是对内的应用,还是对外的应用,对外的应用有包括C端,B端,和第三方客户。故而有下面的最终方案。

实现方案

在对内的网关应用里,引入maven依赖:

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

新增如下XXL-JOB配置类:

@Slf4j
@Configuration
public class XxlJobConfig {
    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;
    @Value("${xxl.job.executor.appname}")
    private String appName;
    @Value("${xxl.job.executor.port:9999}")
    private int port;
    @Value("${xxl.job.accessToken:default_token}")
    private String accessToken;

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        log.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
        executor.setAdminAddresses(adminAddresses);
        executor.setAppname(appName);
        executor.setPort(port);
        executor.setAccessToken(accessToken);
        return executor;
    }
}

对应的,需要在Apollo里新增如下配置。其中有些配置是固定不变的,可以放在本地配置文件里;未来有可能变化的,放在Apollo里。
在这里插入图片描述
这里的appname实际上就是XXL-JOB的执行器:
在这里插入图片描述
gateway服务是以pod形式运行在k8s集群里,不言而喻,采用自动注册这种方式。

网关服务里新增定时任务解析,请求转发配置类:

@Slf4j
@Component
public class XxlJobLogicConfig {
	private static final String URL = "url:";
	private static final String METHOD = "method:";
	private static final String DATA = "data:";
	private static final String GET = "GET";
	private static final String POST = "POST";

    @XxlJob("httpJobHandler")
    public void httpJobHandler() {
    	// 参数解析及校验
        String jobParam = XxlJobHelper.getJobParam();
        if (StringUtils.isBlank(jobParam)) {
            XxlJobHelper.log("param[" + jobParam + "] invalid");
            XxlJobHelper.handleFail();
            return;
        }
        String[] httpParams = jobParam.split("\n");
        String url = "";
        String method = "";
        String data = "null";
        for (String httpParam : httpParams) {
            if (httpParam.startsWith(URL)) {
                url = httpParam.substring(httpParam.indexOf(URL) + URL.length()).trim();
            }
            if (httpParam.startsWith(METHOD)) {
                method = httpParam.substring(httpParam.indexOf(METHOD) + METHOD.length()).trim().toUpperCase();
            }
            if (httpParam.startsWith(DATA)) {
                data = httpParam.substring(httpParam.indexOf(DATA) + DATA.length()).trim();
            }
        }
        if (StringUtils.isBlank(url)) {
            XxlJobHelper.log("url[" + url + "] invalid");
            XxlJobHelper.handleFail();
            return;
        }
        if (!GET.equals(method) && !POST.equals(method)) {
            XxlJobHelper.log("method[" + method + "] invalid");
            XxlJobHelper.handleFail();
            return;
        }
        log.info("xxlJob调度请求url={},请求method={},请求数据data={}", url, method, data);
        // 判断是否为POST请求
        boolean isPostMethod = POST.equals(method);
        HttpURLConnection connection = null;
        BufferedReader bufferedReader = null;
        try {
            URL realUrl = new URL(url);
            connection = (HttpURLConnection) realUrl.openConnection();
            // 设置具体的方法,也就是具体的定时任务
            connection.setRequestMethod(method);
            // POST请求需要output
            connection.setDoOutput(isPostMethod);
            connection.setDoInput(true);
            connection.setUseCaches(false);
            connection.setReadTimeout(900 * 1000);
            connection.setConnectTimeout(600 * 1000);
            // connection:Keep-Alive 表示在一次http请求中,服务器进行响应后,不再直接断开TCP连接,而是将TCP连接维持一段时间。
            // 在这段时间内,如果同一客户端再次向服务端发起http请求,便可以复用此TCP连接,向服务端发起请求。
            connection.setRequestProperty("connection", "keep_alive");
            // Content-Type 表示客户端向服务端发送的数据的媒体类型(MIME类型)
            connection.setRequestProperty("content-type", "application/json;charset=UTF-8");
            // Accept-Charset 表示客户端希望服务端返回的数据的媒体类型(MIME类型)
            connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8");
            // gateway请求转发到其他应用
            connection.connect();
            // 如果是POST请求,则判断定时任务是否含有执行参数
            if (isPostMethod && StringUtils.isNotBlank(data)) {
                DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream());
                // 写参数
                dataOutputStream.write(data.getBytes(Charset.defaultCharset()));
                dataOutputStream.flush();
                dataOutputStream.close();
            }
            int responseCode = connection.getResponseCode();
            // 判断请求转发、定时任务触发是否成功
            if (responseCode != 200) {
                throw new RuntimeException("Http Request StatusCode(" + responseCode + ") Invalid");
            }
            bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), Charset.defaultCharset()));
            StringBuilder stringBuilder = new StringBuilder();
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line);
            }
            String responseMsg = stringBuilder.toString();
            log.info("xxlJob调度执行返回数据={}", responseMsg);
            XxlJobHelper.log(responseMsg);
        } catch (Exception e) {
            XxlJobHelper.log(e);
            XxlJobHelper.handleFail();
        } finally {
            try {
                if (bufferedReader != null) {
                    bufferedReader.close();
                }
                if (connection != null) {
                    connection.disconnect();
                }
            } catch (Exception e) {
                XxlJobHelper.log(e);
            }
        }
    }
}

稍微有点麻烦的是,每个Spring Cloud应用都需要手动新增一个ScheduleController:

/**
 * 定时任务入口,所有服务的@RequestMapping满足/schedule/appName这种格式,方便统一管理
 **/
@RestController
@RequestMapping("/schedule/search")
public class ScheduleController {
    @Resource
    private ChineseEnglishStoreSchedule chineseEnglishStoreSchedule;

    @GetMapping("/chineseEnglishStoreSchedule")
    public Response<Boolean> chineseEnglishStoreSchedule() {
        chineseEnglishStoreSchedule.execute();
        return Response.success(true);
    }
}

另外,需要在gateway网关服务里新增路由转发规则:
在这里插入图片描述
每个有定时任务,且准备接入XXL-JOB平台的SC微服务,都需要新增类似上面截图里的4条配置信息。

优点:所有带有定时任务的服务一目了然,方便统一维护和管理。

这种方案无需改造具体的某个Schedule类:

@JobHander(value = "autoJobHandler")
public class AutoJobHandler extends IJobHandler {
	@Override
    public ReturnT<String> execute(String... params) {
    try {
    	// 既有的业务逻辑
    	// 执行成功
    	return ReturnT.SUCCESS;
    } catch (Exception e) {
            logger.error("execute error id:{}, error info:{}", id, e);
            return ReturnT.FAIL;
        }
        return ReturnT.SUCCESS;
    }
}

最后都省却不了的一个步骤,在XXL-JOB admin管理平台新增一个个任务:
在这里插入图片描述

验证

任务调度的执行日志:
在这里插入图片描述
ELK日志查询平台里也可以搜索到逻辑代码里打印的日志。

参考

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/914245.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于jeecg-boot的flowable流程加签功能实现

更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a; https://gitee.com/nbacheng/nbcio-boot 前端代码&#xff1a;https://gitee.com/nbacheng/nbcio-vue.git 在线演示&#xff08;包括H5&#xff09; &#xff1a; http://122.227.135.243:9888 今天我…

【C++】C++ 引用详解 ① ( 变量的本质 - 引入 “ 引用 “ 概念 | 引用语法简介 | 引用做函数参数 | 复杂类型引用做函数参数 )

文章目录 一、变量的本质 - 引入 " 引用 " 概念1、变量的本质 - 内存别名2、引入 " 引用 " 概念 - 已定义变量的内存别名3、" 引用 " 的优点 二、引用语法简介1、语法说明2、代码示例 - 引用的定义和使用 三、引用做函数参数1、普通引用必须初始…

minikube安装

minikube也是需要docker环境的&#xff0c;首先看一下docker 下载docker.repo源到本地 通过repo里面查找最新的docker 开始安装docker 修改docker 下载加速地址&#xff0c; systemctl deamon-reload 下载minikube minikube start | minikube curl -LO https://storage.goog…

Mybatis(二)映射文件配置与动态SQL

Mybatis&#xff08;二&#xff09;映射文件配置 1.Mybatis映射文件配置 1.入参 1.1.parameterType(了解) CRUD标签都有一个属性parameterType&#xff0c;底层的statement通过它指定接收的参数类型。入参数据有以下几种类型&#xff1a;HashMap&#xff0c;基本数据类型&a…

会计资料基础

会计资料 1.会计要素及确认与计量 1.1 会计基础 1.2 六项会计要素小结 1.3 利润的确认条件 1.3.1 利润的定义和确认条件 1.4 会计要素及确认条件 2.六项会计要素 2.1 资产的特征及其确认条件 这部分资产可以给企业带来经济收益&#xff0c;但是如果不能带来经济利益&#xff…

提升团队合作效率:企业网盘的文件管理和协作利用方法

随着信息技术的飞速发展&#xff0c;企业越来越依赖于网络和云服务来提高工作效率。在这样的背景下&#xff0c;企业网盘作为一种重要的在线存储和协作工具&#xff0c;正在被越来越多的企业所采用。本文将探讨如何利用企业网盘进行文件管理和协作&#xff0c;从而构建高效的团…

Windows快捷键常用介绍,提高工作(摸鱼)效率

一&#xff1a;背景 本文主要是讲解Windows电脑常见的快捷键&#xff0c;包括ctrl快捷键&#xff0c;win快捷键&#xff0c;不管是开发人员还是普通办公人员&#xff0c;都是很方便的。我们平时没事操作都是用鼠标去选择对应的功能&#xff0c;或者在我的电脑--控制面板寻找&a…

把matlab的m文件打包成单独的可执行文件

安装Matlab Compiler Adds-on在app里找到Application Compiler 选择要打包的文件matlab单独的运行程序的话需要把依赖的库做成runtime. 这里有两个选项. 上面那个是需要对方在联网的情况下安装, 安装包较小.下面那个是直接把runtime打包成安装程序, 大概由你的程序依赖的库的多…

谷粒商城环境搭建一:Docker容器部署

Docker容器部署 VMware虚拟机安装 参考&#xff1a;VMware虚拟机安装Linux教程 Docker安装 Linux安装Docker # 1.更新apt包索引 sudo apt-get update# 2.安装以下包以使apt可以通过HTTPS使用存储库&#xff08;repository&#xff09; sudo apt-get install -y apt-transpor…

滑动窗口介绍

1.基本概念 利用单调性&#xff0c;使用同向双指针&#xff0c;两个指针之间形成一个窗口 子串与子数组都是连续的一段子序列时不连续的 2.为什么可以用滑动窗口&#xff1f; 暴力解决时发现两个指针不需要回退&#xff08;没必要回退&#xff0c;一定不会符合结果&#xf…

什么是有效的预测性维护 ?

在现代制造业的背景下&#xff0c;设备的可靠性和生产效率成为了企业追求的关键目标。而预测性维护&#xff08;Predictive Maintenance&#xff0c;简称PdM&#xff09;作为一种先进的维护策略&#xff0c;逐渐成为了实现这些目标的重要工具。然而&#xff0c;什么是有效的预测…

2023年国赛数学建模思路 - 案例:随机森林

文章目录 1 什么是随机森林&#xff1f;2 随机深林构造流程3 随机森林的优缺点3.1 优点3.2 缺点 4 随机深林算法实现 建模资料 ## 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 什么是随机森林&#xff…

AP9235 dc-dc升压恒流电源驱动IC 2.8-30V 输出电流2A SOT23-6

概述 AP9235B 系列是一款固定振荡频率、恒流输出的升压型DC/DC转换器&#xff0c;非常适合于移动电话、PDA、数码相机等电子产品的背光驱动。输出电压可达30V &#xff0c;3.2V输入电压可以驱动六个串联LED&#xff0c; 2.5V输入电压可以驱动两路并联LED&#xff08;每路串联…

你不知道的 malloc 内幕

你不知道的 malloc 内幕 1. 引言&#xff1a;一个例子例1例2 2. 基础概念2.1 内存管理发展过程2.2 虚拟存储器2.3 内存分配机制2.4 VMA2.4.1 进程的 VMA2.4.2 vma 分析 3. 实例分析3.1 malloc 到底干了啥3.2 memset 的偷天换日3.2.1 虚拟地址转物理地址3.2.2 page fault 3.3 fr…

线程池UncaughtExceptionHandler无效?可能是使用方式不对

背景 在业务处理中&#xff0c;使用了线程池来提交任务执行&#xff0c;但是今天修改了一小段代码&#xff0c;发现任务未正确执行。而且看了相关日志&#xff0c;也并未打印结果。 源码简化版如下&#xff1a; 首先&#xff0c;自定义了一个线程池 public class NamedThrea…

iMX6ULL QT环境配置 | CMake在Linux下的交叉编译环境搭建及使用

习惯了使用cmake&#xff0c;再也不想回到手写makefile的年代了。相比手写makefile&#xff0c;使用cmake则像是实现了机动化&#xff0c;管理项目工程的编译变得很简单了。况且cmake很流行&#xff0c;linux下的很多软件源码包&#xff0c;很多也都使用了cmake的方式编译。因此…

大数据课程K4——Spark的DAGRDD依赖关系

文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 了解Spark的DAG; ⚪ 掌握Spark的RDD的依赖关系; ⚪ 了解Spark对于DAG的Stage的划分; 一、DAG概念 1. 概述 Spark会根据用户提交的计算逻辑中的RDD的转换和动作来生成RDD之间的依赖关…

Android JNI系列详解之AS创建Native C++项目

一、前提 Android Studio版本&#xff1a;Android Studio Electric Eel | 2022.1.1 Patch 1 二、创建Native C项目 1.新建项目 2.选择新建Native C项目 3.New Project 4.选择C标准库的支持版本 5.项目自带的默认生成的代码 6.buil.gradle中也自带了CMakeList的配置&#xff08;…

详细了解G1、了解G1、G1垃圾收集器详解、G1垃圾回收器简单调优

4.详细了解G1&#xff1a; 4.1.一&#xff1a;什么是垃圾回收 4.2.了解G1 4.3.G1 Yong GC 4.4.G1 Mix GC 4.5.三色标记算法 4.6.调优实践 5.G1垃圾收集器详解 5.1.G1垃圾收集器 5.2.G1的堆内存划分 5.3.G1的运行过程 5.4.三色标记 5.4.1.漏标问题 5.5.记忆集与卡表 5.6.安全点与…

开发小技巧(逐步完善)

一、验证码 1&#xff09;将 大小写字母 和 数字 存储在字符数组中 2&#xff09;用随机数的方式生成随机码 3&#xff09;采用字符串的方式存储验证码即可 import java.util.Random;public class TEST {public static void main(String[] args) {char[] chs new char[62];//…