Una solución para tareas programadas Spring@Scheduled para acceder a XXL-JOB (basado en SC Gateway)

fondo

La empresa para la que trabajo actualmente mantiene más de 25 proyectos de microservicios distribuidos Spring Cloud. Aproximadamente 10 de ellos tienen microservicios escritos con lógica de tareas programadas, utilizando Spring @Scheduled.

Desventajas de las tareas programadas Spring @Scheduled:

  1. No se admite la agrupación en clústeres: para evitar ejecuciones repetidas, es necesario introducir bloqueos distribuidos
  2. Rígido e inflexible: no admite ejecución manual, ejecución única, ejecución de compensación, modificación de parámetros de tarea, pausa de tareas, eliminación de tareas, modificación del tiempo de programación y reintento en caso de falla.
  3. Sin mecanismo de alarma: no hay ningún mecanismo de alarma después de que falla una tarea. El registro de excepciones de ejecución lógica registra los registros de ERRORES y está conectado a las alarmas de Prometheus. Este método se considera una alarma a nivel de registro, no un mecanismo de alarma a nivel de tarea.
  4. Las tareas de fragmentación no son compatibles: cuando se procesan datos ordenados, las tareas de ejecución de fragmentación en varias máquinas procesan datos diferentes.
  5. ……

En base a esto, considere introducir el marco liviano de programación de tiempo distribuido XXL-JOB, es decir, migrar las tareas programadas a la plataforma XXL-JOB.

Respecto a XXL-JOB, consulte el blog anterior.

Diseño

Teniendo en cuenta que tenemos más de 10 aplicaciones distribuidas SC y más de 30 tareas programadas. Si es necesario migrar y transformar cada aplicación, cada aplicación debe configurar la información relacionada con XXL-JOB. Por supuesto, esto se puede lograr mediante el mecanismo de herencia compartida del espacio de nombres Apollo. Fuera de tema: si tengo tiempo, escribiré un blog sobre la herencia de la configuración del espacio de nombres Apollo más adelante.

En otras palabras, puedo mantener la información de configuración de XXL-JOB en Apollo en una aplicación (una aplicación corresponde a un espacio de nombres de Apollo), y otras aplicaciones pueden lograr la reutilización de la configuración al reutilizar esta aplicación (Apollo).

Pero cada aplicación debe agregar una nueva clase de configuración ¿Cómo reutilizar la clase de configuración? Esto también se puede solucionar. La solución es mantener la clase de configuración en la biblioteca de componentes comunes (es necesario introducir la anotación Spring @Configuration, es decir, spring-contextintroducir el paquete de dependencia), y luego la clase de inicio Spring Boot de cada aplicación debe escanear esta clase de configuración. .

También es necesario modificar las más de 30 clases de tareas programadas @@Component correspondientes a las más de 30 tareas programadas. Todas las aplicaciones de tareas programadas deben introducir dependencias de Maven.

Debe agregar manualmente una clase de tarea programada en XXL-JOB.

Parece una buena solución, pero no descarta que diferentes aplicaciones tengan configuraciones con el mismo nombre, si encuentras configuraciones con el mismo nombre es necesario modificar el nombre de la configuración. La modificación de la clase de inicio Spring Boot puede traer problemas desconocidos.

Finalmente, considere que todas nuestras aplicaciones deben reenviarse a través del servicio Gateway, ya sean aplicaciones internas o aplicaciones externas, las aplicaciones externas incluyen clientes del lado C, del lado B y de terceros. Por lo tanto, tenemos el siguiente plan final.

Plan de IMPLEMENTACION

En la aplicación de puerta de enlace interna, introduzca las dependencias de Maven:

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

Agregue la siguiente clase de configuración 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;
    }
}

En consecuencia, es necesario agregar la siguiente configuración en Apollo. Algunas de las configuraciones son fijas y se pueden colocar en el archivo de configuración local; aquellas que puedan cambiar en el futuro se pueden colocar en Apollo.
Insertar descripción de la imagen aquí
El nombre de la aplicación aquí es en realidad el ejecutor de XXL-JOB:
Insertar descripción de la imagen aquí
el servicio de puerta de enlace se ejecuta en el clúster k8s en forma de pod, y no hace falta decir que se utiliza el registro automático.

Se agregan nuevas clases de configuración de análisis de tareas programadas y reenvío de solicitudes al servicio de puerta de enlace:

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

Lo que es un poco problemático es que cada aplicación Spring Cloud necesita agregar manualmente un 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);
    }
}

Además, es necesario agregar reglas de enrutamiento y reenvío al servicio de puerta de enlace:
Insertar descripción de la imagen aquí
cada microservicio SC que tenga tareas programadas y esté listo para acceder a la plataforma XXL-JOB debe agregar cuatro datos de configuración similares a los de la captura de pantalla anterior.

Ventajas: Todos los servicios con tareas programadas son claros de un vistazo, lo que facilita el mantenimiento y la gestión unificados.

Esta solución no requiere modificar una clase de Horario específica:

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

Al final hay un paso que no se puede omitir: Agregar tareas en la plataforma de gestión de administración de XXL-JOB:
Insertar descripción de la imagen aquí

verificar

Registro de ejecución de programación de tareas:
Insertar descripción de la imagen aquí
los registros impresos en el código lógico también se pueden buscar en la plataforma de consulta de registros ELK.

referencia

Supongo que te gusta

Origin blog.csdn.net/lonelymanontheway/article/details/132307385
Recomendado
Clasificación