SPRING BOOT 动态定时任务

本文介绍如何使用Spring Boot实现动态定时任务,包括原理、设计思路及代码实现。同时,还提供了一种利用Redis分布式锁解决多服务环境下定时任务重复执行问题的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

SPRING BOOT 动态定时任务

0.前言

传统spring定时任务采用的是@Sechedu注解去实现,但是该注解只能指定固定的时间任务,例如:配置了2s执行一次,那么永远只能是每两秒执行一次

但是在有些特殊场景下需要一些动态定时任务,例如:最初配置了2s执行一次,在执行任务中,修改配置为5秒执行一次,那么就需要动态的加载配置,使任务动态的变成5s执行一次

1.原理

要想实现动态定时任务,就需要借助SpringSchedulingConfigurer接口,该方式可以实现动态时间,定时任务,具体原理如下图:

2.Cron设计

如下图所示

定时任务数据库表设计

3. 配置解释

3.1 SchedulingConfigurer

要想实现定时任务主要是实现SchedulingConfigurer接口,然后重写configureTasks方法

3.2 ScheduledTaskRegistrar

configureTasks方法中,ScheduledTaskRegistrar通过addTriggerTask来添加触发器任务,从而去执行。而addTriggerTask方法有两个参数,一个是要执行得任务,一个是触发器

spring容器加载工具类

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * spring容器加载工具类
 *
 * @author lcc
 */
@Component
public class SpringContext implements ApplicationContextAware {

  private static ApplicationContext context;

  @Override
  public void setApplicationContext(ApplicationContext context) throws BeansException {
    SpringContext.setContext(context);
  }

  public static ApplicationContext getContext() {
    return context;
  }

  private static void setContext(ApplicationContext context) {
    SpringContext.context = context;
  }

  public static Object getBean(String beanName) {
    Object obj = getContext().getBean(beanName);
    return obj;
  }

  public static <T> T getBean(String beanName, Class<T> type) {
    T obj = getContext().getBean(beanName, type);
    return obj;
  }

  public static <T> T getBean(Class<T> type) {
    T obj = getContext().getBean(type);
    return obj;
  }

  public static <T> Map<String, T> getBeansOfType(Class<T> type) {
    final Map<String, T> beansOfType = getContext().getBeansOfType(type);
    return beansOfType;
  }

  public static void pushEvent(ApplicationEvent event) {
    context.publishEvent(event);
  }
}

具体配置代码如下:

package net.test.api.job;


import io.lettuce.core.dynamic.support.ReflectionUtils;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.List;
import net.test.demo.common.framework.SpringContext;
import net.test.dao.entity.demo.ScheduleSetting;
import net.test.dao.mapper.demo.ScheduledSettingMapper;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;

@Component
public class CronSchedule implements SchedulingConfigurer {

  @Autowired
  private ScheduledSettingMapper scheduledSettingMapper;

  @Override
  public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
    // 获取状态为1的任务
    List<ScheduleSetting> scheduleList = scheduledSettingMapper.getScheduleList();
    System.out.println(scheduleList.size());
    if (CollectionUtils.isNotEmpty(scheduleList)){
      for (ScheduleSetting s : scheduleList){
        scheduledTaskRegistrar.addTriggerTask(getRunnable(s), getTrigger(s));
      }
    }
  }

  /**
   * 转换首字母小写
   *
   * @param str
   * @return
   */
  public static String lowerFirstCapse(String str) {
    char[] chars = str.toCharArray();
    chars[0] += 32;
    return String.valueOf(chars);
  }

  private Runnable getRunnable(ScheduleSetting scheduleSetting){
    return new Runnable() {
      @Override
      public void run() {
        Class<?> clazz;
        try {
          clazz = Class.forName(scheduleSetting.getBeanName());
          String className = lowerFirstCapse(clazz.getSimpleName());
          Object bean = SpringContext.getBean(className);
          Method method = ReflectionUtils.findMethod(bean.getClass(), scheduleSetting.getMethodName());
          if (StringUtils.isNotEmpty(scheduleSetting.getMethodParams())) {
            ReflectionUtils.invokeMethod(method, bean, scheduleSetting.getMethodParams());
          } else {
            ReflectionUtils.invokeMethod(method, bean);
          }
        } catch (ClassNotFoundException e) {
          e.printStackTrace();
        }
      }
    };
  }

  private Trigger getTrigger(ScheduleSetting scheduleSetting){
    return new Trigger() {
      @Override
      public Date nextExecutionTime(TriggerContext triggerContext) {
        CronTrigger trigger = new CronTrigger(scheduleSetting.getCronExpression());
        Date nextExec = trigger.nextExecutionTime(triggerContext);
        return nextExec;
      }
    };
  }
}

 注意:上面的需要重启服务才能应用新的任务变更,为了解决这个问题可以使用下面的方式

4.变更任务管理器

ThreadPoolTaskScheduler 是 spring taskSchedule 接口的实现,用于管理任务的调度和取消。当检测到任务配置发生变化时(比如执行时间改变,状态变化),需要先取消原有的任务调度,然后再根据新的配置重新注册任务。

  • schedule(Runnable task, Date stateTime),在指定时间执行一次定时任务

  • schedule(Runnable task, Trigger trigger),动态创建指定表达式cron的定时任务

  • scheduleAtFixedRate,指定间隔时间执行一次任务,间隔时间为前一次执行开始到下次任务开始时间

  • scheduleWithFixedDelay,指定间隔时间执行一次任务,间隔时间为前一次任务完成到下一次开始时间

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;

/**
 * 动态任务管理器,用于调度和取消定时任务
 *
 * @author hudy
 * @date 2024/9/11 10:28
 */
@Service
public class DynamicTaskManager {

  /**
   * 线程池任务调度器,用于管理任务的调度
   */
  private final ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
  /**
   * 用ConcurrentHashMap线程安全地存储已调度的任务,键为任务ID,值为任务的未来对象
   */
  private final Map<Long, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();

  /**
   * 构造函数,初始化任务调度器
   */
  public DynamicTaskManager() {
    // 设置线程池大小
    taskScheduler.setPoolSize(10);
    // 初始化
    taskScheduler.initialize();
  }

  /**
   * 获取任务调度器
   *
   * @return 任务调度器实例
   */
  public ThreadPoolTaskScheduler getTaskScheduler() {
    return taskScheduler;
  }

  /**
   * 调度任务更新
   *
   * @param taskId         任务ID
   * @param task           Runnable任务
   * @param cronExpression Cron表达式,用于定义任务的调度时间
   * @param status         任务状态,1表示启用,0表示禁用
   */
  public void scheduleTask(Long taskId, Runnable task, String cronExpression, Integer status) {
    // 如果任务已存在,则先取消旧的任务
    cancelTask(taskId);
    if (status.equals(0)) {
      return;
    }
    // 创建新的CronTrigger
    CronTrigger trigger = new CronTrigger(cronExpression);
    // 调度新任务
    ScheduledFuture<?> future = taskScheduler.schedule(task, trigger);
    // 将任务ID与未来对象关联起来,以便后续取消任务
    scheduledTasks.put(taskId, future);
  }

  /**
   * 取消任务
   *
   * @param taskId 任务ID
   */
  public void cancelTask(Long taskId) {
    if (scheduledTasks.containsKey(taskId)) {
      // 取消任务:传入true作为参数表示即使任务正在运行尝试中断它;传入false则表示只应取消尚未开始的任务
      scheduledTasks.get(taskId).cancel(false);
      // 从任务映射中移除任务
      scheduledTasks.remove(taskId);
    }
  }
}

及时生效版

@Component
public class CronSchedule implements SchedulingConfigurer {

  @Autowired
  private ScheduledSettingMapper scheduledSettingMapper;

  @Autowired
  private DynamicTaskManager dynamicTaskManager;

  @Override
  public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
    scheduledTaskRegistrar.setScheduler(dynamicTaskManager.getTaskScheduler());
  }

  /**
   * 每1分钟刷新一次。如果做了任务管理功能页面,增删改查同步调用(scheduleTask来更新任务)。注解换成PostConstruct
   */
  @Scheduled(fixedDelay = 60000)
  public void refreshScheduleTasks() {
    List<ScheduleSetting> tasks = arkScheduledSettingMapper.getAll();
    System.out.println("作业数:" + tasks.size());
    for (ScheduleSetting task : tasks) {
      dynamicTaskManager.scheduleTask(task.getId(), getRunnable(task), task.getCronExpression(), task.getJobStatus());
    }
  }

  /**
   * 转换首字母小写
   *
   * @param str
   * @return
   */
  public static String lowerFirstCapse(String str) {
    char[] chars = str.toCharArray();
    chars[0] += 32;
    return String.valueOf(chars);
  }

  private Runnable getRunnable(ScheduleSetting scheduleSetting){
    return new Runnable() {
      @Override
      public void run() {
        Class<?> clazz;
        try {
          clazz = Class.forName(scheduleSetting.getBeanName());
          String className = lowerFirstCapse(clazz.getSimpleName());
          Object bean = SpringContext.getBean(className);
          Method method = ReflectionUtils.findMethod(bean.getClass(), scheduleSetting.getMethodName());
          if (StringUtils.isNotEmpty(scheduleSetting.getMethodParams())) {
            ReflectionUtils.invokeMethod(method, bean, scheduleSetting.getMethodParams());
          } else {
            ReflectionUtils.invokeMethod(method, bean);
          }
        } catch (ClassNotFoundException e) {
          e.printStackTrace();
        }
      }
    };
  }
}

定时任务重复执行解决方案

在开发的过程中,经常会遇到需要使用定时器的问题,比如需要定时向任务表写任务。但是项目是部署到集群环境下的,如果不做处理,就会出现定时任务重复执行的问题。问题产生的原因:由于我们项目同时部署在多台集群机器上,因此到达指定的定时时间时,多台机器上的定时器可能会同时启动,造成重复数据或者程序异常等问题
该问题的解决方案可能有以下几种,仅供参考:

  • 一、指定机器执行包含定时器任务

该方案操作简单,但只适合临时解决以下,正常生产环境应该没人会采取该方案。

  • 二、数据库加锁

通过数据库的锁机制,来获取任务执行状态,先更新,后执行的原则,可以实现避免任务重新执行的目标。

  • 三、redis的过期机制和分布式锁

我们最终决定采用的方案。面向切面编程,通过注解控制

package net.config;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

/**
 * redis分布式锁解决集群服务器定时任务重复执行的问题,类文件支持多个方法配置
 *
 * @author hudy
 */
@Aspect
@Slf4j
@Component
public class CacheLockAspect {

    private static final String LOCK_VALUE = "locked";

    @Resource
    private RedisTemplate redisTemplate;

    @Around("execution(* *.*(..)) && @annotation(com.iris.websocket.annotation.CacheLock)")
    public void cacheLockPoint(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method cacheMethod = signature.getMethod();
        if(null == cacheMethod) {
            log.info("未获取到使用方法 pjp: {}", pjp);
            throw new RuntimeException("未获取到使用方法 pjp");
        }
        boolean lock = false;
        String lockKey = cacheMethod.getAnnotation(CacheLock.class).lockedPrefix();
        long timeOut = cacheMethod.getAnnotation(CacheLock.class).expireTime();
        boolean release = cacheMethod.getAnnotation(CacheLock.class).release();
        if (StringUtils.isBlank(lockKey)) {
            throw new RuntimeException(String.format("method:%s没有配置lockedPrefix", cacheMethod));
        }
        try {
            lock = redisTemplate.opsForValue().setIfAbsent(lockKey, LOCK_VALUE, timeOut, TimeUnit.SECONDS);
            if (lock) {
                log.info("method:{}获取锁:{},开始运行!", cacheMethod, lockKey);
                pjp.proceed();
                return;
            }
            log.info("method:{} Lock被占用不执行此轮任务!", cacheMethod);
            release = false;
        } catch (RedisConnectionFailureException e) {
            log.error("method:{},运行错误!", cacheMethod, e);
            throw new RuntimeException("获取redis定时任务锁失败");
        } catch (Throwable e) {
            //pjp.proceed(); 抛出的业务异常
            throw new RuntimeException(e);
        } finally {
            if (lock && release) {
                log.info("method:{}释放锁:{},执行完成!", cacheMethod, lockKey);
                releaseLock(lockKey, LOCK_VALUE);
            }
        }
    }

    /**
     * 释放分布式锁
     */
    public boolean releaseLock(String key, String value) {
        boolean flag = false;
        String releaseScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end";
        Long success = 1L;
        try {
            Long result = (Long) redisTemplate.execute(
                (RedisConnection connection) -> connection.eval(
                    releaseScript.getBytes(),
                    ReturnType.INTEGER,
                    1,
                    key.getBytes(),
                    value.getBytes())
            );
            flag = success.equals(result);
        } catch (Exception e) {
            log.error("释放锁异常, key = {}", key, e);
        }
        return flag;
    }
}

自定义注解

package com.iris.websocket.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheLock {

  String lockedPrefix() default ""; //缓存锁key
  long expireTime() default 10;     //缓存过期时间,单位:秒
  boolean release() default true;   //执行完成释放锁,不释放锁过期也会释放
}

使用方法

@CacheLock(lockedPrefix = "gtCallConnRateTask", expireTime = 10)
public void gtCallConnRateTask() {
    log.info("推送定时任务开始执行------------");
    ordersService.gtCallConnRateTask();
    log.info("推送定时任务执行完毕------------");
}

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值