A solution for Spring@Scheduled scheduled tasks to access XXL-JOB (based on SC Gateway)

background

The company I currently work for maintains 25+ Spring Cloud distributed microservice projects. About 10 of them have microservices written with scheduled task logic, using Spring @Scheduled.

Disadvantages of Spring @Scheduled scheduled tasks:

  1. Clustering is not supported: To avoid repeated execution, distributed locks need to be introduced
  2. Rigid and inflexible: does not support manual execution, single execution, compensation execution, modifying task parameters, pausing tasks, deleting tasks, modifying scheduling time, and retrying on failure
  3. No alarm mechanism: There is no alarm mechanism after a task fails. Logical execution exception recording ERROR logs are connected to Prometheus alarms. This is considered a log-level alarm, not a task-level alarm mechanism.
  4. Sharding tasks are not supported: when processing ordered data, multi-machine sharding execution tasks process different data.
  5. ……

Based on this, consider introducing the lightweight distributed timing scheduling framework XXL-JOB, that is, migrating scheduled tasks to the XXL-JOB platform.

Regarding XXL-JOB, please refer to the previous blog.

Design

Considering that we have 10+ SC distributed applications and 30+ scheduled tasks. If each application needs to be migrated and transformed, each application needs to configure XXL-JOB related information. Of course, this can be achieved through the Apollo namespace shared inheritance mechanism. Off topic: If I have time, I will write a blog about Apollo namespace configuration inheritance later.

In other words, I can maintain the configuration information of XXL-JOB in Apollo in an application (an application corresponds to an Apollo namespace), and other applications can achieve configuration reuse by reusing this application (Apollo).

But each application must add a new configuration class. How to reuse the configuration class? This can also be solved. The solution is to maintain the configuration class in the commons component library (the Spring @Configuration annotation needs to be introduced, that is, the spring-contextdependency package is introduced), and then the Spring Boot startup class of each application needs to scan this configuration class.

It is also necessary to modify the 30+ @@Component scheduled task classes corresponding to the 30+ scheduled tasks. All scheduled task applications need to introduce maven dependencies.

You have to manually add a scheduled task class in XXL-JOB.

It seems like a good solution, but it does not rule out that different applications have configurations with the same name. If you encounter configurations with the same name, you need to modify the configuration naming. Spring Boot startup class modification may bring unknown problems.

Finally, consider that all our applications need to be forwarded through the Gateway service, whether they are internal applications or external applications. External applications include C-side, B-side, and third-party customers. Therefore, we have the following final plan.

Implementation plan

In the internal gateway application, introduce maven dependencies:

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

Add the following XXL-JOB configuration class:

@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;
    }
}

Correspondingly, the following configuration needs to be added in Apollo. Some of the configurations are fixed and can be placed in the local configuration file; those that may change in the future can be placed in Apollo.
Insert image description here
The appname here is actually the executor of XXL-JOB:
Insert image description here
the gateway service runs in the k8s cluster in the form of a pod. It goes without saying that automatic registration is used.

New scheduled task analysis and request forwarding configuration classes are added to the gateway service:

@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);
            }
        }
    }
}

What is a little troublesome is that each Spring Cloud application needs to manually add a 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);
    }
}

In addition, routing and forwarding rules need to be added to the gateway service:
Insert image description here
each SC microservice that has scheduled tasks and is ready to access the XXL-JOB platform needs to add four pieces of configuration information similar to the screenshot above.

Advantages: All services with scheduled tasks are clear at a glance, which facilitates unified maintenance and management.

This solution does not require modifying a specific Schedule class:

@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;
    }
}

In the end, there is a step that cannot be omitted. Add tasks in the XXL-JOB admin management platform:
Insert image description here

verify

Task scheduling execution log:
Insert image description here
The logs printed in the logic code can also be searched in the ELK log query platform.

reference

Guess you like

Origin blog.csdn.net/lonelymanontheway/article/details/132307385