Spring定时任务开发指南(动态实现)
Spring定时任务开发指南(动态实现)
一、任务调度概览
定时任务的原理可以概括为以下几个核心点:
-
- 任务调度机制
-
定时任务的核心是调度器(Scheduler),它负责按照预设的时间规则触发任务执行。
-
调度器通常维护一个任务队列,根据任务的执行时间排序,并在到达指定时间时将其提交给线程池执行。
-
- 时间轮询与触发
-
调度器通过轮询或事件驱动的方式检查当前时间是否满足某个任务的触发条件。
-
对于基于固定间隔的任务(如
fixedRate或fixedDelay),调度器会计算下次执行时间并加入任务队列。 -
对于基于 Cron 表达式的任务,调度器解析 Cron 规则,确定具体的执行时间点。
-
- 线程管理
-
定时任务通常运行在线程池中,避免阻塞主线程。
-
Spring 默认使用单线程执行
@Scheduled任务,但可以通过配置自定义线程池(如TaskScheduler)实现并发执行。 -
- 持久化与容错
-
简单的定时任务(如
@Scheduled)通常不支持持久化,服务重启后任务状态丢失。 -
高级框架(如 Quartz)支持将任务信息存储到数据库中,具备故障恢复和集群部署能力。
-
- 分布式协调
-
在分布式环境中,多个节点可能同时运行相同的定时任务,导致重复执行。
-
解决方案包括使用分布式锁(如 Redis)、Quartz 集群模式或外部调度中心(如 XXL-JOB)来保证任务唯一性。
总结来说,定时任务的本质是通过调度器按照时间规则触发任务执行,并借助线程池和持久化机制保障任务的可靠性和性能。
1、常规注解标记
-
- 配置类或启动类上标记开启任务@EnableScheduling
-
- bean组件的方法使用@Scheduled标记定时任务
-
- 默认使用单线程调度,可以使用@Async做异步调用
2、多线程调度
-
创建多线程任务调度器ThreadPoolTaskScheduler
-
底层本质也是利用JDK的调度线程池
ScheduledExecutorService -
提高任务并发处理能力
@Configuration public class SchedulerConfig { @Bean public ThreadPoolTaskScheduler taskScheduler () { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler (); scheduler.setPoolSize( 8 ); // 设置线程池大小 scheduler.setThreadNamePrefix( "scheduled-task-" ); // 设置线程名称前缀 return scheduler; } }
3、TaskSchedule注册时机
-
TaskSchedulerBean 的注册时机
-
默认情况下 :
如果在 Spring Boot 项目中启用了定时任务功能(例如通过
@EnableScheduling注解),Spring 会自动注册一个默认的TaskSchedulerBean。这个 Bean 是在 Spring 应用上下文刷新阶段注册的,也就是在 Spring 启动过程中完成的。 -
手动配置 :
如果需要自定义TaskScheduler,可以通过配置类显式地定义一个TaskSchedulerBean。此时,Spring 会在应用启动时加载你自定义的 Bean。@Configuration @EnableScheduling public class SchedulingConfig { @Bean public TaskScheduler taskScheduler () { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler (); scheduler.setPoolSize( 10 ); // 设置线程池大小 scheduler.setThreadNamePrefix( "custom-scheduler-" ); return scheduler; } }
-
TaskScheduler的实现原理
TaskScheduler是 Spring 提供的一个接口,用于调度任务的执行。它的核心实现类是ThreadPoolTaskScheduler,其工作原理如下:
(1)核心组件
- 线程池 :
ThreadPoolTaskScheduler内部封装了一个ScheduledExecutorService,用于管理和执行定时任务。线程池的大小可以通过setPoolSize()方法配置。 - 任务调度机制 :
通过schedule()方法将任务提交到线程池中,并根据指定的时间规则(如固定频率、延迟执行、Cron 表达式等)触发任务执行。
(2)任务调度流程
-
- 任务提交 :
用户通过TaskScheduler的schedule()方法提交任务,传入Runnable或Callable以及调度规则(如CronTrigger或PeriodicTrigger)。
- 任务提交 :
-
- 时间计算 :
调度器根据传入的调度规则计算任务的下次执行时间。
- 时间计算 :
-
- 任务执行 :
当到达执行时间时,调度器从线程池中分配一个线程来执行任务。
- 任务执行 :
(3)关键方法
-
schedule(Runnable task, Trigger trigger):
根据Trigger规则调度任务(如 Cron 表达式)。 -
scheduleAtFixedRate(Runnable task, long period):
以固定频率执行任务(不考虑任务执行时间)。 -
scheduleWithFixedDelay(Runnable task, long delay):
在任务完成后延迟指定时间再次执行。 -
- 与
@Scheduled的关系
- 与
-
@Scheduled注解底层也是依赖TaskScheduler来实现任务调度的。 -
如果没有显式配置
TaskScheduler,Spring 会使用默认的单线程调度器(ConcurrentTaskScheduler)来执行@Scheduled任务。 -
自定义
TaskScheduler可以为@Scheduled提供更强的并发能力和更灵活的配置。
-
- 总结
-
TaskScheduler默认在 Spring 启动时注册,前提是启用了调度功能(如@EnableScheduling)。 -
其实现基于线程池和任务调度机制,核心是
ThreadPoolTaskScheduler。 -
可通过自定义
TaskSchedulerBean 来满足特定需求,例如调整线程池大小或设置任务执行策略。
二、ThreadPoolTaskScheduler详解
ThreadPoolTaskScheduler 是 Spring 框架中用于任务调度的核心实现类之一,它基于 Java 的 ScheduledExecutorService 构建,提供了强大的定时任务调度能力。以下是其底层原理的详细分析:
1、核心组成
ThreadPoolTaskScheduler 的底层依赖主要包括以下组件:
- 线程池 (
ScheduledExecutorService)
内部封装了一个ScheduledThreadPoolExecutor,用于管理和执行任务。线程池的大小可以通过setPoolSize(int poolSize)方法配置。 - 任务队列
用于存放待执行的任务,支持延迟任务和周期性任务。 - 调度机制
基于ScheduledExecutorService的schedule()、scheduleAtFixedRate()和scheduleWithFixedDelay()方法实现任务调度。
2、关键方法实现原理
(1)schedule(Runnable task, Trigger trigger)
- 作用 :根据
Trigger规则调度任务。 - 实现原理:
- 解析
Trigger获取下次执行时间。 - 使用
ScheduledExecutorService.schedule()方法将任务提交到线程池中。 - 任务执行完成后,重新计算下次执行时间并递归调度。
(2)scheduleAtFixedRate(Runnable task, long period)
- 作用:以固定频率执行任务(不考虑任务执行耗时)。
- 实现原理:
- 直接调用
ScheduledExecutorService.scheduleAtFixedRate()方法。 - 任务会严格按照设定的时间间隔执行,即使前一个任务未完成也会继续调度下一个任务。
(3)scheduleWithFixedDelay(Runnable task, long delay)
- 作用:在任务完成后延迟指定时间再次执行。
- 实现原理:
- 调用
ScheduledExecutorService.scheduleWithFixedDelay()方法。 - 下次任务的调度时间 = 上次任务结束时间 + 延迟时间。
(4)execute(Runnable task)
- 作用:立即执行任务。
- 实现原理:
- 将任务提交到内部线程池中执行,相当于异步调用。
3、线程池管理
- 线程池初始化
在ThreadPoolTaskScheduler初始化时,会创建一个ScheduledThreadPoolExecutor实例,默认线程池大小为 1。 - 线程池销毁
通过destroy()方法关闭线程池,释放资源。Spring 在容器关闭时会自动调用此方法。 - 线程命名
可通过setThreadNamePrefix(String threadNamePrefix)设置线程名称前缀,便于调试和监控。
4、异常处理机制
- 默认行为
如果任务执行过程中抛出异常,默认不会中断调度流程,后续任务仍会正常执行。 - 自定义异常处理器
可通过setErrorHandler(ErrorHandler errorHandler)方法设置全局异常处理器,统一捕获和处理任务中的异常。
5、与其他调度器的区别
| 特性 | ThreadPoolTaskScheduler | ConcurrentTaskScheduler |
|---|---|---|
| 线程池支持 | 支持多线程 | 单线程 |
| 并发能力 | 强 | 弱 |
| 适用场景 | 高并发、复杂调度任务 | 简单任务 |
6、典型使用场景
- 动态任务调度(如 Web 页面配置定时任务)
- 高并发环境下的周期性任务执行
- 需要精确控制任务执行时间的场景
7、源码关键点
以下是 ThreadPoolTaskScheduler 中几个关键方法的核心逻辑片段(简化版):
// schedule 方法的核心实现 public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) { return this .scheduledExecutor.schedule( () -> { task.run(); // 递归调度下次执行 schedule(task, trigger); }, trigger.nextExecutionTime( null ), TimeUnit.MILLISECONDS ); } // scheduleAtFixedRate 的核心实现 public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period) { return this .scheduledExecutor.scheduleAtFixedRate(task, 0 , period, TimeUnit.MILLISECONDS); }
ThreadPoolTaskScheduler 通过封装 ScheduledExecutorService 提供了灵活且高效的定时任务调度能力,其核心优势在于:
- 支持多种调度策略(固定频率、延迟执行、Cron 表达式等)
- 可配置线程池大小,适应高并发场景
- 提供完善的异常处理和资源管理机制
三、ScheduledTaskRegistrar源码
- 可以在运行时添加删除任务
- 实现任务调度配置接口,动态注册(配置)任务
- 其核心注册器
ScheduledTaskRegistrar
1、调度配置器
@FunctionalInterface public interface SchedulingConfigurer { /** * Callback allowing a { @link org.springframework.scheduling.TaskScheduler} * and specific { @link org.springframework.scheduling.config.Task} instances * to be registered against the given the { @link ScheduledTaskRegistrar}. * @param taskRegistrar the registrar to be configured */ void configureTasks (ScheduledTaskRegistrar taskRegistrar) ; }
里面具有非常多实现的方法,正如所想:添加、删除、修改任务的List;
并且task分类:cron任务、fixRate任务、delayRate任务、oneTime任务;
没有自定义任务调度器,则创建单线程调度器处理。
上文中,已经创建了多线程任务调度器ThreadPoolTaskScheduler,这里只需要调用方法即可。
2、调度方法实现
/** * Helper bean for registering tasks with a { @link TaskScheduler}, typically using cron * expressions. * * <p>{ @code ScheduledTaskRegistrar} has a more prominent user-facing role when used in * conjunction with the { @link * org.springframework.scheduling.annotation.EnableAsync @EnableAsync } annotation and its * { @link org.springframework.scheduling.annotation.SchedulingConfigurer * SchedulingConfigurer} callback interface. * * @see org.springframework.scheduling.annotation.EnableAsync * @see org.springframework.scheduling.annotation.SchedulingConfigurer */ public class ScheduledTaskRegistrar implements ScheduledTaskHolder , InitializingBean, DisposableBean { /** * A special cron expression value that indicates a disabled trigger: { @value }. * <p>This is primarily meant for use with { @link #addCronTask (Runnable, String)} * when the value for the supplied { @code expression} is retrieved from an * external source — for example, from a property in the * { @link org.springframework.core.env.Environment Environment}. * @since 5.2 * @see org.springframework.scheduling.annotation.Scheduled #CRON _DISABLED */ public static final String CRON_DISABLED = "-" ; @Nullable private TaskScheduler taskScheduler; @Nullable private ScheduledExecutorService localExecutor; @Nullable private ObservationRegistry observationRegistry; @Nullable private List<TriggerTask> triggerTasks; @Nullable private List<CronTask> cronTasks; @Nullable private List<IntervalTask> fixedRateTasks; @Nullable private List<IntervalTask> fixedDelayTasks; @Nullable private List<DelayedTask> oneTimeTasks; private final Map<Task, ScheduledTask> unresolvedTasks = new HashMap <>( 16 ); private final Set<ScheduledTask> scheduledTasks = new LinkedHashSet <>( 16 ); /** * Set the { @link TaskScheduler} to register scheduled tasks with. */ public void setTaskScheduler (TaskScheduler taskScheduler) { Assert.notNull(taskScheduler, "TaskScheduler must not be null" ); this .taskScheduler = taskScheduler; } /** * Set the { @link TaskScheduler} to register scheduled tasks with, or a * { @link java.util.concurrent.ScheduledExecutorService} to be wrapped as a * { @code TaskScheduler}. */ public void setScheduler ( @Nullable Object scheduler) { if (scheduler == null ) { this .taskScheduler = null ; } else if (scheduler instanceof TaskScheduler ts) { this .taskScheduler = ts; } else if (scheduler instanceof ScheduledExecutorService ses) { this .taskScheduler = new ConcurrentTaskScheduler (ses); } else { throw new IllegalArgumentException ( "Unsupported scheduler type: " + scheduler.getClass()); } } /** * Return the { @link TaskScheduler} instance for this registrar (may be { @code null}). */ @Nullable public TaskScheduler getScheduler () { return this .taskScheduler; } /** * Return whether this { @code ScheduledTaskRegistrar} has any tasks registered. * @since 3.2 */ public boolean hasTasks () { return (!CollectionUtils.isEmpty( this .triggerTasks) || !CollectionUtils.isEmpty( this .cronTasks) || !CollectionUtils.isEmpty( this .fixedRateTasks) || !CollectionUtils.isEmpty( this .fixedDelayTasks) || !CollectionUtils.isEmpty( this .oneTimeTasks)); } /** * Calls { @link #scheduleTasks ()} at bean construction time. */ @Override public void afterPropertiesSet () { scheduleTasks(); } /** * Schedule all registered tasks against the underlying * { @linkplain #setTaskScheduler (TaskScheduler) task scheduler}. */ protected void scheduleTasks () { if ( this .taskScheduler == null ) { this .localExecutor = Executors.newSingleThreadScheduledExecutor(); this .taskScheduler = new ConcurrentTaskScheduler ( this .localExecutor); } if ( this .triggerTasks != null ) { for (TriggerTask task : this .triggerTasks) { addScheduledTask(scheduleTriggerTask(task)); } } if ( this .cronTasks != null ) { for (CronTask task : this .cronTasks) { addScheduledTask(scheduleCronTask(task)); } } if ( this .fixedRateTasks != null ) { for (IntervalTask task : this .fixedRateTasks) { if (task instanceof FixedRateTask fixedRateTask) { addScheduledTask(scheduleFixedRateTask(fixedRateTask)); } else { addScheduledTask(scheduleFixedRateTask( new FixedRateTask (task))); } } } if ( this .fixedDelayTasks != null ) { for (IntervalTask task : this .fixedDelayTasks) { if (task instanceof FixedDelayTask fixedDelayTask) { addScheduledTask(scheduleFixedDelayTask(fixedDelayTask)); } else { addScheduledTask(scheduleFixedDelayTask( new FixedDelayTask (task))); } } } if ( this .oneTimeTasks != null ) { for (DelayedTask task : this .oneTimeTasks) { if (task instanceof OneTimeTask oneTimeTask) { addScheduledTask(scheduleOneTimeTask(oneTimeTask)); } else { addScheduledTask(scheduleOneTimeTask( new OneTimeTask (task))); } } } } private void addScheduledTask ( @Nullable ScheduledTask task) { if (task != null ) { this .scheduledTasks.add(task); } } /** * Return all locally registered tasks that have been scheduled by this registrar. * @since 5.0.2 * @see #addTriggerTask * @see #addCronTask * @see #addFixedRateTask * @see #addFixedDelayTask */ @Override public Set<ScheduledTask> getScheduledTasks () { return Collections.unmodifiableSet( this .scheduledTasks); } @Override public void destroy () { for (ScheduledTask task : this .scheduledTasks) { task.cancel( false ); } if ( this .localExecutor != null ) { this .localExecutor.shutdownNow(); } } }
四、动态任务实现
- 动态添加任务
- 动态刷新任务
- 适配多租户
- 使用数据库持久化任务
- 添加任务后会取消原有任务,然后重新注册任务
- 该接口
SchedulingConfigurer的方法configureTasks执行启动时由spring执行一次 - 所以如果刷新时需要,则得使用变量存下来
- 执行任务的逻辑封装起来,baseTask模版方法
- 根据bean名称执行对应的任务
1、动态任务配置
@Slf4j @Component public class TaskConfig implements SchedulingConfigurer { private final ApplicationContext applicationContext; private final TaskMapper taskMapper; private final TaskStateMapper taskStateMapper; private final ThreadPoolTaskScheduler scheduler; private final MultiTenantProperties multiTenantProperties; private final TenantDatasourceManager tenantDatasourceManager; private ScheduledTaskRegistrar registrar; private final AtomicBoolean isRefreshing = new AtomicBoolean ( false ); private final AtomicInteger refreshRequestCount = new AtomicInteger ( 0 ); private Map<String, ScheduledTask> scheduledTaskMap = new ConcurrentHashMap <>(); @Value("${task-schedule.enable:true}") private Boolean enable = true ; public TaskConfig (ApplicationContext applicationContext, TaskMapper taskMapper, TaskStateMapper taskStateMapper, ThreadPoolTaskScheduler scheduler, MultiTenantProperties multiTenantProperties, TenantDatasourceManager tenantDatasourceManager) { this .applicationContext = applicationContext; this .taskMapper = taskMapper; this .taskStateMapper = taskStateMapper; this .scheduler = scheduler; this .multiTenantProperties = multiTenantProperties; this .tenantDatasourceManager = tenantDatasourceManager; } @Override public void configureTasks (ScheduledTaskRegistrar scheduledTaskRegistrar) { // 定时任务开关 if (!enable) { log.info( "============>> 定时任务未开启 <<===========" ); return ; } if (registrar == null ) { registrar = scheduledTaskRegistrar; } scheduledTaskRegistrar.setScheduler(scheduler); QueryWrapper<Task> queryWrapper = new QueryWrapper <>(); queryWrapper.eq( "enabled" , 1 ); // 开启多租户时,需要从各个租户拉取定时任务,加到任务调度列表 if (multiTenantProperties.getEnabled()) { List<String> datasourceList = tenantDatasourceManager.getDatasourceList(); if (!CollectionUtils.isEmpty(datasourceList)) { log.info( "============>> 正在配置定时任务(SAAS模式) <<=============" ); for (String datasourceName : datasourceList) { if ( "master" .equals(datasourceName)) { continue ; } tenantDatasourceManager.changeDataSourceByName(datasourceName); List<Task> currentList = taskMapper.selectList(queryWrapper); List<UpdateTaskeStateDTO> updateStateList = new ArrayList <>(); if (!CollectionUtils.isEmpty(currentList)) { for (Task task : currentList) { UpdateTaskeStateDTO updateDto = new UpdateTaskeStateDTO (task.getId()); try { // 添加多租户定时任务 BaseTask taskBean = (BaseTask)applicationContext.getBean(task.getBeanName()); MultiTenantTask multiTenantTask = new MultiTenantTask (datasourceName, taskBean, task, tenantDatasourceManager); CronTask cronTask = new CronTask (multiTenantTask, task.getCron()); ScheduledTask scheduledTask = scheduledTaskRegistrar.scheduleCronTask(cronTask); log.info( "添加定时任务成功 - {} - {} - {} - {}" , datasourceName, task.getTaskName(), task.getBeanName(), task.getCron()); // 将调度任务保存下来,以便之后关闭 scheduledTaskMap.put(datasourceName+ "-" +task.getId(), scheduledTask); updateDto.setState(TaskStateType.NORMAL.getCode()); updateDto.setNextExecuteStartTime(CronPatternUtil.nextDateAfter(CronPattern.of(task.getCron()), new Date (), true )); } catch (Exception e) { log.error( "添加定时任务失败 - {} - {}: {}" , datasourceName, task.getTaskName(), e.getLocalizedMessage()); updateDto.setState(TaskStateType.ERROR.getCode()); } updateStateList.add(updateDto); } if (!updateStateList.isEmpty()) { taskStateMapper.updateState(updateStateList); } } } tenantDatasourceManager.clear(); } } else { // 非多租户 List<Task> taskList = taskMapper.selectList(queryWrapper); log.info( "============>> 正在配置定时任务: 共{}条 <<=============" , taskList.size()); List<UpdateTaskeStateDTO> updateStateList = new ArrayList <>(); for (Task task : taskList) { UpdateTaskeStateDTO updateDto = new UpdateTaskeStateDTO (task.getId()); try { // 添加定时任务 BaseTask taskBean = (BaseTask)applicationContext.getBean(task.getBeanName()); taskBean.setTaskInfo(task); CronTask cronTask = new CronTask (taskBean, task.getCron()); ScheduledTask scheduledTask = scheduledTaskRegistrar.scheduleCronTask(cronTask); log.info( "添加定时任务成功 - {} - {} - {}" , task.getTaskName(), task.getBeanName(), task.getCron()); // 将调度任务保存下来,以便之后关闭 scheduledTaskMap.put( "" +task.getId(), scheduledTask); updateDto.setState(TaskStateType.NORMAL.getCode()); updateDto.setNextExecuteStartTime(CronPatternUtil.nextDateAfter(CronPattern.of(task.getCron()), new Date (), true )); } catch (Exception e) { log.error( "添加定时任务失败 - {}: {}" , task.getTaskName(), e.getLocalizedMessage()); updateDto.setState(TaskStateType.ERROR.getCode()); } updateStateList.add(updateDto); } if (!updateStateList.isEmpty()) { taskStateMapper.updateState(updateStateList); } } } public void refreshTasks () { if (isRefreshing.compareAndSet( false , true )) { log.info( "============>> 开始刷新任务列表 <<=============" ); scheduledTaskMap.values().forEach(task -> task.cancel( false )); scheduledTaskMap.clear(); configureTasks(registrar); isRefreshing.set( false ); if (refreshRequestCount.getAndSet( 0 ) > 0 ) { refreshTasks(); } } else { refreshRequestCount.getAndAdd( 1 ); log.info( "============>> [跳过]当前已在刷新任务列表 <<=============" ); } } public void runTask (String beanName) { this .scheduler.execute((BaseTask)applicationContext.getBean(beanName)); } public void runTask (Task task) { BaseTask taskBean = (BaseTask)applicationContext.getBean(task.getBeanName()); taskBean.setTaskInfo(task); this .scheduler.execute(taskBean); } }
2、封装BaseTask
-
使用模版方法,封装流程
-
子类实现具体逻辑
-
使用redisson分布式锁,可以在分布式环境、集群多节点环境下使用该框架
@Slf4j public abstract class BaseTask implements Runnable { @Autowired protected TaskService taskService; @Autowired protected RedissonClient redissonClient; protected String beanName = getClass().getSimpleName(); protected final static String TASK_LOCK_NAME = "task:lock:" ; // 多租户时使用 private final ThreadLocal<Task> currentTask = new ThreadLocal <>(); private final ThreadLocal<String> currentDatasource = new ThreadLocal <>(); // 非多租户时使用 @Setter private Task taskInfo; public Task getTaskInfo () { if (currentTask.get() != null ) { return currentTask.get(); } if (taskInfo != null ) { return taskInfo; } else { throw new IllegalArgumentException ( "任务信息为空" ); } } public String getArgs () { return getArgs( true ); } public String getArgs ( boolean throwIfNull) { String args = getTaskInfo().getArgs(); if (args != null ) return args; if (throwIfNull) throw new IllegalArgumentException ( "参数为空" ); return null ; } public void setCurrentDatasource (String datasource) { currentDatasource.set(datasource); } public void setCurrentTask (Task task) { currentTask.set(task); } @Override public void run () { RLock rLock = redissonClient.getLock(TASK_LOCK_NAME + uniqueId()); try { // 添加分布式锁以支持集群部署,多服务器部署时不会同时执行 if (rLock.tryLock( 0 , 10 , TimeUnit.SECONDS)) { log.info( "定时任务[{}][{}]获取锁成功,开始执行任务" , getTaskInfo().getTaskName(), this .currentDatasource.get()); boolean isSuccess = false ; LocalDateTime startTime = LocalDateTime.now(); long useTime = 0L ; String errorMsg = "" ; boolean isUpdate = taskService.setTaskRunning(getTaskInfo().getId()); if (!isUpdate) { log.info( "定时任务[{}][{}]已在其它机器执行过,跳过本次执行" , getTaskInfo().getTaskName(), this .currentDatasource.get()); return ; } try { // 执行任务内容 this .execute(); // 记录任务状态 isSuccess = true ; useTime = LocalDateTimeUtil.between(startTime, LocalDateTime.now()).toMillis(); log.info( "定时任务[{}][{}]执行成功,用时{}ms" , getTaskInfo().getTaskName(), this .currentDatasource.get(), useTime); } catch (Exception e) { isSuccess = false ; errorMsg = e.getLocalizedMessage().length() <= 500 ? e.getLocalizedMessage() : e.getLocalizedMessage().substring( 0 , 500 ); log.error( "定时任务[{}][{}]执行失败:{}" , getTaskInfo().getTaskName(), this .currentDatasource.get(), e.getStackTrace()); } finally { // 写任务日志 taskService.writeTaskLog(getTaskInfo().getId(), isSuccess, startTime, useTime, errorMsg); } } else { log.warn( "定时任务[{}][{}]获取锁失败,跳过任务" , getTaskInfo().getTaskName(), this .currentDatasource.get()); } } catch (Exception e) { log.error( "定时任务[{}][{}]运行失败:{}" , getTaskInfo().getTaskName(), this .currentDatasource.get(), e.getStackTrace()); } finally { // 确保由同一个线程解锁 if (rLock.isHeldByCurrentThread()) { rLock.unlock(); // 安全解锁 } if (currentDatasource.get() != null ) { currentDatasource.remove(); } if (currentTask.get() != null ) { currentTask.remove(); } } } public abstract void execute () ; protected String uniqueId () { String datasource = currentDatasource.get(); if (StringUtil.hasText(datasource)) { return datasource + "-" + beanName; } else { return beanName; } } }
子类实现
-
继承
BaseTask -
实现子类具体逻辑
@Slf4j @Component("ConsumeSettleTask") @RequiredArgsConstructor public class ConsumeSettleTask extends BaseTask { private final ConsumeOrderService consumeOrderService; /** * 消费结算: 每5分钟执行一次,处理20分钟(后期可配置)前的订单 */ @Override public void execute () { consumeOrderService.settleByTask(); } }
3、redis订阅通知
-
使用redis进行简单的消息通知
-
多节点能同时感知,进行刷新本地任务
package com.water.demo.task.config; import com.water.demo.task.constant.RedisChannel; import com.water.demo.task.subscriber.MessageSubscriber; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.listener.PatternTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; @Configuration public class RedisMessageConfig { @Bean RedisMessageListenerContainer container (RedisConnectionFactory connectionFactory, MessageListenerAdapter taskListenerAdapter) { RedisMessageListenerContainer container = new RedisMessageListenerContainer (); container.setConnectionFactory(connectionFactory); // 订阅频道 container.addMessageListener(taskListenerAdapter, new PatternTopic (RedisChannel.REFRESH_TASKS)); return container; } @Bean MessageListenerAdapter taskListenerAdapter (MessageSubscriber receiver) { return new MessageListenerAdapter (receiver, "handleMessage" ); } }
消息订阅
@Slf4j @Component public class MessageSubscriber { private final TaskConfig taskConfig; public MessageSubscriber (TaskConfig taskConfig) { this .taskConfig = taskConfig; } public void handleMessage (String message) { // 收到消息后,进行本地任务刷新 log.info( "Received message: {}" , message); taskConfig.refreshTasks(); } }
消息触发
-
页面任务添加、修改、删除时触发
-
事务提交后发送redis消息
@Override @Transactional public Boolean update (TaskUpdateDTO dto, LoginUser loginUser) { Preconditions.checkNotNull(dto.getTaskName(), "任务名称不能为空" ); Preconditions.checkNotNull(dto.getBeanName(), "Bean名称不能为空" ); Preconditions.checkNotNull(dto.getCron(), "Cron表达式不能为空" ); Preconditions.checkArgument(!dto.getCron().matches(cronRegex), "Cron表达式格式不正确" ); LocalDateTime now = LocalDateTime.now(); Task task = baseMapper.selectById(dto.getId()); Preconditions.checkNotNull(task, "任务不存在" ); task.setTaskName(dto.getTaskName()); task.setBeanName(dto.getBeanName()); task.setCron(dto.getCron()); task.setArgs(dto.getArgs()); task.setEnabled(dto.getEnabled()); task.setDescription(dto.getDescription()); task.setUpdatedBy(loginUser.getUserId()); task.setUpdatedTime(now); baseMapper.updateById(task); QueryWrapper<TaskState> taskStateQueryWrapper = new QueryWrapper <>(); taskStateQueryWrapper.eq( "task_id" , task.getId()); TaskState taskState = taskStateMapper.selectOne(taskStateQueryWrapper); if (taskState == null ) { taskState = new TaskState (); taskState.setTaskId(task.getId()); taskState.setState(dto.getEnabled() ? TaskStateType.DEFINE.getCode() : TaskStateType.STOPPED.getCode()); taskStateMapper.insert(taskState); } else { taskState.setState(dto.getEnabled() ? TaskStateType.DEFINE.getCode() : TaskStateType.STOPPED.getCode()); taskStateMapper.updateById(taskState); } TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization () { @Override public void afterCommit () { redisService.getRedisTemplate().convertAndSend( RedisChannel.REFRESH_TASKS, "UPDATE_TASK:" + task.getBeanName() ); log.debug( "事务提交后发送UPDATE消息: {}" , task.getBeanName()); } }); return true ; }
五、长短轮询设计
要使用定时任务实现短轮询和长轮询,并具备任务可取消的能力,可以采用以下方案:
1、 核心设计思路
- 使用
ScheduledExecutorService管理定时任务,支持任务的调度和取消。 - 短轮询通过固定频率执行任务,在满足条件时主动取消。
- 长轮询通过延迟调度实现,每次执行后重新调度下一次任务。
- 通过
Future对象管理任务的生命周期,实现任务的取消功能。
2.、代码实现
(1)定义任务管理器
import java.util.concurrent.*; public class PollingTaskManager { private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool( 2 ); private volatile Future<?> shortPollingTask; private volatile Future<?> longPollingTask; // 启动短轮询 public void startShortPolling (String orderId, int maxAttempts, long intervalMillis) { shortPollingTask = scheduler.scheduleAtFixedRate(() -> { PaymentStatus status = queryPaymentResult(orderId); if (status == PaymentStatus.SUCCESS || status == PaymentStatus.FAILED) { handleFinalResult(status); cancelAllTasks(); // 查询完成,取消所有任务 } }, 0 , intervalMillis, TimeUnit.MILLISECONDS); // 设置超时取消机制 scheduler.schedule(() -> { if (shortPollingTask != null && !shortPollingTask.isCancelled()) { shortPollingTask.cancel( true ); startLongPolling(orderId); // 超时后启动长轮询 } }, maxAttempts * intervalMillis, TimeUnit.MILLISECONDS); } // 启动长轮询 public void startLongPolling (String orderId) { longPollingTask = scheduler.scheduleWithFixedDelay(() -> { PaymentStatus status = queryPaymentResult(orderId); if (status == PaymentStatus.SUCCESS || status == PaymentStatus.FAILED) { handleFinalResult(status); cancelAllTasks(); // 查询完成,取消所有任务 } }, 0 , 1800 , TimeUnit.SECONDS); // 每30分钟执行一次 } // 取消所有任务 public void cancelAllTasks () { if (shortPollingTask != null && !shortPollingTask.isCancelled()) { shortPollingTask.cancel( true ); } if (longPollingTask != null && !longPollingTask.isCancelled()) { longPollingTask.cancel( true ); } } // 模拟查询支付结果 private PaymentStatus queryPaymentResult (String orderId) { // 实际逻辑:调用支付接口查询结果 return PaymentStatus.PENDING; // 示例返回值 } // 处理最终结果 private void handleFinalResult (PaymentStatus status) { System.out.println( "支付结果:" + status); } // 关闭调度器 public void shutdown () { scheduler.shutdown(); } }
(2)枚举定义支付状态
enum PaymentStatus { SUCCESS, FAILED, PENDING }
3、使用示例
public class Main { public static void main (String[] args) { PollingTaskManager manager = new PollingTaskManager (); String orderId = "123456" ; int maxAttempts = 30 ; // 最大尝试次数 long intervalMillis = 4000 ; // 每次查询间隔4秒 // 启动短轮询 manager.startShortPolling(orderId, maxAttempts, intervalMillis); // 模拟程序运行一段时间后手动取消任务 try { Thread.sleep( 10000 ); // 运行10秒后取消 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } manager.cancelAllTasks(); manager.shutdown(); } }
4、关键特性说明
- 任务可取消 :通过
Future.cancel(true)方法中断正在执行的任务。 - 短轮询超时机制 :利用
scheduler.schedule()设置超时时间,超时后自动切换到长轮询。 - 资源释放 :通过
shutdown()方法优雅关闭线程池,避免资源泄漏。 - 灵活性:支持动态调整轮询参数(如最大次数、间隔时间等)。
方案兼顾了功能性和可维护性,适用于支付结果查询等需要轮询的场景。
六、调度方式比较
在Spring项目中使用定时任务非常简单,主要可以通过以下几种方式实现:
1. 使用 @Scheduled注解
这是最常见的方式,适用于简单的定时任务。
-
- 启用定时任务支持
在配置类上添加@EnableScheduling注解。
@Configuration @EnableScheduling public class SchedulingConfig { }
- 启用定时任务支持
-
- 定义定时任务方法
在需要执行定时任务的方法上添加@Scheduled注解,并指定执行规则。
@Component public class ScheduledTasks { // 每5秒执行一次 @Scheduled(fixedRate = 5000) public void reportCurrentTime () { System.out.println( "当前时间: " + new Date ()); } // 每分钟的第0秒执行 @Scheduled(cron = "0 * * * * ?") public void cronTask () { System.out.println( "Cron任务执行时间: " + new Date ()); } }
- 定义定时任务方法
常用参数说明:
fixedRate: 固定频率执行(单位毫秒)。fixedDelay: 上次任务完成后延迟指定时间再执行。-
cron\]: 使用 Cron 表达式定义复杂的调度规则。
2. 使用TaskScheduler接口
适用于更灵活的任务调度需求。
@Component public class DynamicTaskScheduler { @Autowired private TaskScheduler taskScheduler; public void scheduleTask () { taskScheduler.schedule(() -> { System.out.println( "动态任务执行时间: " + new Date ()); }, new Date (System.currentTimeMillis() + 5000 )); // 5秒后执行 } }
3. 使用 Quartz 调度框架
对于复杂的企业级定时任务,推荐使用 Quartz。
-
- 引入依赖
< dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter-quartz </ artifactId > </ dependency >
-
- 创建 Job 类
public class MyJob implements Job { @Override public void execute (JobExecutionContext context) { System.out.println( "Quartz任务执行时间: " + new Date ()); } }
-
- 配置调度器
@Configuration public class QuartzConfig { @Bean public JobDetail jobDetail () { return JobBuilder.newJob(MyJob.class) .withIdentity( "myJob" , "group1" ) .storeDurably() .build(); } @Bean public Trigger trigger () { return TriggerBuilder.newTrigger() .forJob(jobDetail()) .withIdentity( "myTrigger" , "group1" ) .startNow() .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds( 10 ) .repeatForever()) .build(); } }
4. 总结对比
| 方式 | 适用场景 | 特点 |
|---|---|---|
| @Scheduled | 简单定时任务 | 配置简单,适合轻量级任务 |
| TaskScheduler | 动态调度任务 | 更加灵活,支持运行时调度 |
| Quartz | 复杂企业级任务 | 功能强大,支持持久化和集群 |
根据实际需求选择合适的方式即可!如果只是简单的定时任务,建议优先使用 @Scheduled;若涉及分布式或高可用场景,则推荐使用 Quartz。
七、调度框架Quartz
Quartz 的底层也是基于 JDK 的 ScheduledExecutorService 实现任务调度的。
-
核心机制 :Quartz 内部使用了类似
ScheduledExecutorService的线程池机制来管理任务的调度和执行。 -
扩展性 :相比原生的
ScheduledExecutorService,Quartz 提供了更丰富的功能,例如: -
支持复杂的调度规则(如 Cron 表达式)。
-
任务持久化(可存储到数据库中)。
-
集群部署和高可用性支持。
-
底层依赖 :Quartz 的调度器(
Scheduler)最终会将任务委托给线程池执行,而这个线程池的实现可以基于ScheduledExecutorService或其他自定义线程池。
因此,虽然 Quartz 功能更强大,但其底层仍然依赖于 JDK 提供的基础调度能力。
Quartz 实现集群和高可用的机制主要依赖于数据库和分布式锁:
1、集群实现原理
-
- 数据库共享
-
所有节点共享同一个数据库,用于存储任务信息(如
QRTZ_JOB_DETAILS、QRTZ_TRIGGERS等表)。 -
每个节点通过数据库获取任务调度信息,确保任务状态一致性。
-
- 分布式锁机制
-
使用数据库行级锁(如
SELECT ... FOR UPDATE)防止多个节点同时执行同一个任务。 -
通过
QRTZ_LOCKS表管理集群级别的锁,确保只有一个节点能获取调度权。 -
- 节点发现与负载均衡
-
节点通过定期心跳检测彼此状态。
-
任务会被均匀分配到各个节点执行,避免单点压力。
2、高可用实现原理
-
- 故障转移
-
当某个节点宕机时,其他节点会接管其未完成的任务。
-
通过数据库中的
INSTANCE_NAME字段识别节点归属,自动清理失效节点的状态。 -
- 任务恢复
-
节点重启后,会从数据库中恢复未完成的任务并重新调度。
-
支持任务的持久化存储,避免因节点崩溃导致任务丢失。
3、何时需要集群
- 高并发场景:单节点无法处理大量定时任务。
- 负载分散:需要将任务分布到多个节点执行。
- 容灾需求:避免单点故障影响整体服务。
4、高可用体现的场景
-
- 节点宕机
-
其他节点自动接管宕机节点的任务,保证任务不中断。
-
- 网络分区
-
通过数据库状态同步,确保各节点间数据一致。
-
- 任务失败重试
-
支持任务失败后的自动重试机制,提升系统健壮性。
这种设计使得 Quartz 在分布式环境下具备强大的容错能力和任务调度能力。