Cómo diseñar un programador distribuido de tareas cronometradas usted mismo

¡Continúe creando, acelere el crecimiento! Este es el tercer día de mi participación en el "Nuggets Daily New Plan · October Update Challenge", haz clic para ver los detalles del evento

Por qué usar un programador distribuido

El programador distribuido se utiliza principalmente en la programación de tiempo de algunas tareas en el sistema. Usualmente diseñamos una tarea cronometrada, la forma más fácil es @scheduledconfigurar la tarea cronometrada directamente con anotaciones, para que el trabajo de desarrollo también sea simple. Pero puede haber una situación, si sucede en el entorno de producción, la hora de la tarea programada debe cambiarse sin reiniciar, o tal vez necesitamos cerrar una determinada tarea programada por algún motivo, entonces no puede ser dinámica en este momento. . El programador distribuido puede resolver muy bien estos problemas difíciles.

Algunas personas pueden preguntar: ahora hay algunos programadores de código abierto populares, por ejemplo xxl-job, ¿por qué necesita diseñar el suyo propio? De hecho, no podemos decir que el diseño de código abierto no sea bueno. La razón es que sus funciones son demasiado perfectas. Si se usa bien, alguien necesita especializarse en operación y mantenimiento. Las funciones son demasiado poderosas y la mayoría de las funciones son de mal gusto. Por lo tanto, desarrollamos un conjunto de servicios de programación simples. A veces todavía es necesario.

Proceso de programación distribuida

En primer lugar, el programador distribuido necesita confiar en la configuración de la base de datos, principalmente para configurar la interfaz del servicio de programación y el tiempo de programación. Obtenga la configuración de la base de datos a través del clúster del servicio de programación y analice las tareas que deben programarse. Dado que es un clúster del servicio de trabajo (que también se puede implementar en una sola máquina), también se debe considerar el bloqueo para evitar múltiples jobservicios. programar una tarea varias veces al mismo tiempo. El servicio final jobanalizará la interfaz del servicio e iniciará la programación de tareas para la interfaz del servicio de la aplicación cuando se detecte el punto de activación.imagen.png

Análisis detallado del diseño de la programación distribuida

Diseño de base de datos

Diseño de la tabla Job_info : Registra principalmente la configuración de algunas tareas del trabajo, analicemos los campos principales:

  • job_cron: configuración del tiempo de disparo de la tarea programada
  • config_id: la clave principal de la configuración de la interfaz del servicio de la tarea de programación asociada
  • execute_timeout: configuración del tiempo de espera de la programación de tareas, para evitar que el tiempo de programación sea demasiado largo sin resultados
  • execute_fail_retry_count: el número de reintentos si falla la programación de la tarea
  • job_status: Programación de la configuración del interruptor de estado de la tarea
  • trigger_last_time:最后一次调度时间
  • trigger_next_time:下一次执行时间

imagen.png

job_config表设计:主要配置一些任务对应需要调度的服务接口信息。

  • execute_servier:所需调度的应用服务
  • execute_method:调度应用服务接口
  • execute_param:调度参数配置
  • service_type:服务类型(GET/POST)

imagen.png

job服务调度流程设计

  • 读取配置:首先job服务需要不断的读取数据库配置,从而得知有哪一些任务需要进行调度。可以通过一个while循环加上休眠一段时间不断读取配置,下面就用简短的伪代码做个思路分析:
while(true) {
    // PRE_READ_TIME每次刷新时间间隔
    TimeUnit.MILLISECONDS.sleep(PRE_READ_TIME - System.currentTimeMillis() % 1000);
    // 读取配置,给定一个时间,获取这段时间内要执行调度的任务以及初次配置的任务trigger_next_time=0
    List<JobInfo> jobInfos = jobInfoMapper.select(time);
    // 循环对任务一一进行解析
    // 1.job应用对获取到的任务进行加锁,防止job集群其他服务同时调用,如果确定只会有单机部署可不加锁
    int resultCount = jobInfoMapper.updateByOptimisticLock(jobInfo);
    // 2.加锁成功继续执行下一步,对首次配置的任务(trigger_next_time=0)需要获取job_cron进行解析,计算出真实的下次执行时间trigger_next_time
     refreshNextValidTime(jobInfo, new Date(nowTime));
    // 3.即将要执行的任务加入队列
   checkHighFrequency(jobInfo, nowTime);
}

private void checkHighFrequency(JobInfo jobInfo, Long nowTime) throws ParseException {
    // PRE_READ_TIME = 5000,即提前预留5秒,将任务加入队列
    if (jobInfo.getTriggerNextTime() < (nowTime + PRE_READ_TIME)) {
        // 将任务放入待执行队列
        triggerPoolHelper.triggerJob(jobInfo, jobInfo.getTriggerNextTime() - nowTime);
        // 任务加入队列后,再次更新计算下次调度job的时间
        refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
        // 判断是否是超高频繁任务,即调度周期小于5s一次
        checkHighFrequency(jobInfo, nowTime);
    }
}
// 计算下次执行时间
private void refreshNextValidTime(JobInfo jobInfo, Date fromTime) throws ParseException {
    // 时间表达式转换计算下次触发时间
    Date nextValidTime = new CronExpression(jobInfo.getJobCron()).getNextValidTimeAfter(fromTime);
    if (nextValidTime != null) {
        jobInfo.setTriggerLastTime(jobInfo.getTriggerNextTime());
        jobInfo.setTriggerNextTime(nextValidTime.getTime());
    }
}
复制代码
  • 线程池队列执行任务调度
public void triggerJob(JobInfo jobInfo, long delay) {
    JobInfo copyOf = new JobInfo();
    BeanUtils.copyProperties(jobInfo, copyOf);
    JobTriggerThread triggerThread = new JobTriggerThread(copyOf, tinyJobExecutor.get(jobInfo.getJobType()));
    // 小于0说明是延期的任务,立即执行
    if (delay <= 0) {
        // 加入线程池
        triggerPool.execute(triggerThread);
    }
    // 大于0说明还未到调度时间,延迟调度
    else {
        triggerPool.schedule(triggerThread, delay, TimeUnit.MILLISECONDS);
    }
}
复制代码
  • 任务调度流程:SpringCloud服务使用DiscoveryClient,根据job_config表配置的服务名获取集群服务列表,再根据随机(或自定义算法)计算获取一个服务实例,用该实例创建请求并发起服务接口调用,最终再根据调用结果进行日志记录,以及失败后续是否进行重试处理。
List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances(jobConfig.getExecuteService());
// 随机获取服务列表(可自定义算法)
ServiceInstance serviceInstance = getRandomInstrance(serviceInstanceList);
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建请求
HttpPost httpPost = new HttpPost(serviceInstance.getUri() + "/" + jobConfig.getExecuteMethod() + "?" + jobConfig.getExecuteParam());
// http发起调用
CloseableHttpResponse response = httpClient.execute(httpPost);
复制代码

案例配置说明

  • 添加两个定时任务配置,此配置如有需要也可开发个简单的页面方便配置添加与更改。 imagen.png

  • 定时任务对应的调度服务接口配置 imagen.png

根据以上的配置,定时刷新获取任务列表,任务首次配置trigger_next_time=0,需解析成具体执行时间点,任务调度判断该时间点是否达到可执行时间,在达到指定时间点job服务将对该接口发起调用并记录调度日志。

总结

使用分布式调度器能够很好的管理我们的定时任务接口,开发人员也只需专注开发业务接口,让业务与配置完全分离。定时配置还可以根据业务场景统一进行时间协调管理,以免在有些时间点多任务同时处理,可以将时间配置的分散点以减轻CPU的压力。如果系统业务量少,定时任务也不多的情况也没必要多浪费时间开发一个调度系统。

Supongo que te gusta

Origin juejin.im/post/7149817451960598559
Recomendado
Clasificación