批处理定时执行任务_记一次Spring定时任务非预期执行的解决与原理

ebcf4553939c98efaf3dde91abdc6c73.png

今天一起从一个小bug来看下, Spring定时任务是如何处理的. 一次非预期任务 预定义的任务很简单, 每隔1s执行一次. 代码如下:
@Scheduled(fixedDelay = 1000)    public void syncUser() {  String uuid = UUID.randomUUID().toString();  try {    Thread.sleep(2000);          } catch (InterruptedException e) {    e.printStackTrace();          }          log.info("syncUser success:{}", uuid);}
但观察日志发现, 有的任务执行间隔并不是1s, 同时可以观察到, 多个task是使用的同一线程执行的, 完全不符合预期.
2020-09-17 20:57:20.750  INFO 75127 --- [pool-1-thread-1] com.in.task.Task2                       : syncUser success:1f4ad20c-541a-41c8-8fd0-c9a4a6fd612c2020-09-17 20:57:30.755  INFO 75127 --- [pool-1-thread-1] com.in.task.Tasks                        : cleanUser does not get locker lockKey:1, uuid:2052fe42-b06a-4424-a028-7136b43922152020-09-17 20:57:32.761  INFO 75127 --- [pool-1-thread-1] com.in.task.Task2                       : syncUser success:5be5d07c-991a-442d-a026-e36fcfb0a2fc2020-09-17 20:57:35.767  INFO 75127 --- [pool-1-thread-1] com.in.task.Task2                       : syncUser success:7dea98ec-df3a-4c8f-84f4-aced84f25c742020-09-17 20:57:38.774  INFO 75127 --- [pool-1-thread-1] com.in.task.Task2                       : syncUser success:31fe1753-9467-4956-99b9-fcb134a736ab
解决方式很简单, 自定义定时任务配置, 其中包括定时任务线程池.
@Configurationpublic class ScheduleConfiguration implements SchedulingConfigurer {  @Override  public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {    scheduledTaskRegistrar.setScheduler(setTaskExecutors());  }  @Bean(destroyMethod="shutdown")  public Executor setTaskExecutors(){    return Executors.newScheduledThreadPool(20);  }}
添加配置后, 观察日志, 任务能正常运行, 并且各任务也不会相互影响.
2020-09-17 20:59:28.324  INFO 75215 --- [pool-1-thread-1] com.in.task.Task2                       : syncUser success:71bd8fd4-8690-4db9-b159-10645a53a2e62020-09-17 20:59:31.332  INFO 75215 --- [pool-1-thread-7] com.in.task.Task2                       : syncUser success:ac8dee9f-a155-4d21-a83d-5aa0048622322020-09-17 20:59:33.316  INFO 75215 --- [pool-1-thread-5] com.in.task.Tasks                        : cleanUser does not get locker lockKey:1, uuid:01459057-2274-4681-80f3-e5be3c20561b2020-09-17 20:59:34.338  INFO 75215 --- [pool-1-thread-2] com.in.task.Task2                       : syncUser success:244e8157-2536-407c-9bcf-614ea0106b502020-09-17 20:59:37.346  INFO 75215 --- [ool-1-thread-10] com.in.task.Task2                       : syncUser success:449b6e72-9d9c-4474-a469-4f3763bb9474
问题虽然解决了, 但 知其然,还要知其所以然 . 下面就一起看看Spring是如何管理定时任务的, 为什么加个配置就解决了. Spring定时任务 从定时任务注解 @EnableScheduling 入手, 看下spring启动时都做了什么.
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Import(SchedulingConfiguration.class)@Documentedpublic @interface EnableScheduling {}
继续, 看定时任务配置类: SchedulingConfiguration
@Configuration@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public class SchedulingConfiguration {   @Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)   @Role(BeanDefinition.ROLE_INFRASTRUCTURE)   public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {      return new ScheduledAnnotationBeanPostProcessor();   }}
再继续, 查看定时任务 初始化的核心类及方法
ScheduledAnnotationBeanPostProcessor.postProcessAfterInitialization.(Object bean, String beanName)
该方法会遍历所有Bean中, 查找@Scheduled注解及对应的方法 , 并处理.
public Object postProcessAfterInitialization(Object bean, String beanName) {// ...      Map annotatedMethods = MethodIntrospector.selectMethods(targetClass,            (MethodIntrospector.MetadataLookup<set>) method -> {set// 遍历bean               Set scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(                     method, Scheduled.class, Schedules.class);               return (!scheduledMethods.isEmpty() ? scheduledMethods : null);            });// ...         // Non-empty set of methods      annotatedMethods.forEach((method, scheduledMethods) ->// 处理定时任务方法      scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));      if (logger.isDebugEnabled()) {            logger.debug(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +                  "': " + annotatedMethods);      }   return bean;}
再继续看 processScheduled ()如何处理定时任务的 根据@Scheduled注解参数, 分成不同种类的定时任务 , 并 登记 到ScheduledTaskRegistrar类中处理.
  • cronTask

  • fixedRageTask

  • fixedDelayTask

protected void processScheduled(Scheduled scheduled, Method method, Object bean) {     try {         Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled");         Method invocableMethod = AopUtils.selectInvocableMethod(method, bean.getClass());         // 定时任务封装成线程                     Runnable runnable = new ScheduledMethodRunnable(bean, invocableMethod);// ...        String cron = scheduled.cron();        if (StringUtils.hasText(cron)) {          // 添加cronTask          tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));        }        String fixedDelayString = scheduled.fixedDelayString();        if (StringUtils.hasText(fixedDelayString)) {        // 添加fixedDelayTask            tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));        }        if (StringUtils.hasText(fixedRateString)) {            // 添加fixedRateTask            tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));        }}
登记定时任务 在ScheduledTaskRegistrar中, 当前还没有初始化线程池, 只登记任务, 不执行.
public ScheduledTask scheduleFixedDelayTask(FixedDelayTask task) {              ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);              boolean newTask = false;              if (scheduledTask == null) {                     scheduledTask = new ScheduledTask(task);                     newTask = true;              }              // 放入定时任务线程池中执行              if (this.taskScheduler != null) {                            scheduledTask.future =‍‍this.taskScheduler.scheduleWithFixedDelay(task.getRunnable(), task.getInterval());              }              else {              // 没有线程池, 只登记定时任务                     addFixedDelayTask(task);                     this.unresolvedTasks.put(task, scheduledTask);              }              return (newTask ? scheduledTask : null);       }
启动定时任务 所以定时任务已经登记好了, 剩下的就是启动定时任务了. 任务启动方法:
ScheduledAnnotationBeanPostProcessor.afterSingletonsInstantiated()
里面只调用一个方法, 初始化定时任务登记器 (ScheduledTaskRegistrar)
finishRegistration()
定时任务的线程池, 首先会查找SchedulingConfigurer配置, 初始化ScheduledTaskRegistrar, 包括初始化定时任务线程池. 如果Spring不能从SchedulingConfigurer配置中初始化线程池, 那Spring会尝试从全局范围内查找一个线程池的Bean实例, 但很遗憾, 在我的服务中并没有预定义的线程池.
private void finishRegistration() {// ...     if (this.beanFactory instanceof ListableBeanFactory) {      Mapbeans =((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);                     Listconfigurers = new ArrayList<>(beans.values());                     AnnotationAwareOrderComparator.sort(configurers);         for (SchedulingConfigurer configurer : configurers) {              configurer.configureTasks(this.registrar);         }      }      if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {                             // ...           this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));            }           this.registrar.afterPropertiesSet();}
在定时任务记录器中, 启动各任务
ScheduledTaskRegistrar.afterPropertiesSet();
执行scheduleTasks() 方法, 初始化只有一个核心线程的定时任务线程池, 并添加定时任务. 我们的问题就是Spring自己创建的线程池不能提供足够的线程, 导致多个任务不能并行执行, 各task任务互相影响.
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) {        addScheduledTask(scheduleFixedRateTask(task));      }    }    if (this.fixedDelayTasks != null) {      for (IntervalTask task : this.fixedDelayTasks) {        addScheduledTask(scheduleFixedDelayTask(task));      }    }  }
到此, 整个线程任务就能正常运行了. 流程图 代码涉及到多个类的反复调用, 不容易理解. 可参考下面的时序图理解 初始化: 89a259168f2f9e550cc4a3b04472a62d.png 定时任务启动: 定时任务线程池的初始化为3种, 上面代码流程中都有详解, 这里再汇总下,
  • SchedulingConfigurer自定义配置

  • Spring从全局中寻找的线程池实例

  • 任务启动时, 创建的默认池Executors.newSingleThreadScheduledExecutor();

b2ea123d0e89be9c8f4c5acfe07d7b94.png 推荐阅读
  • MySQL之MVCC实现原理
  • 一分钟搞懂JWT
  • MySQL表的物理设计
  • 一分钟搞懂HTTPS
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值