[spring]task标签及定时任务相关

一、 前言

本篇文章主要分为以下三部分:

  • applicationContext.xml-task命名空间中各配置项的回顾及梳理;
  • Spring Task定时任务的实现(注解方式);
  • Spring Task的问题及相关解决方式;

本次学习使用的Spring的版本:4.3.15。

二、Spring Task

1. 半注解半XML配置

  前面我们依次重新梳理了beans,mvc,context等命名空间的各项配置,今天我们再来看一下task命名空间的一些配置项。简单来说,task命名空间是Spring Framework中用于支持定时任务和异步调用的。首先,要引入task命名空间:

xmlns:task="http://www.springframework.org/schema/task

http://www.springframework.org/schema/task 
http://www.springframework.org/schema/task/spring-task.xsd
1.1 task:annotation-driven标签

该标签是task命名空间中最基础的标签,用于开启定时任务和异步调用的注解支持,来简单看下该注解的几个属性:

  1. scheduler,配置对应的定时任务对象,如果没有配置,默认将使用TaskScheduler实例;
  2. executor,配置对应的异步执行的对象,如果没有配置,默认将使用SimpleAsyncTaskExecutor实例;
  3. exception-handler,在异步执行期间,抛出的异常的实例,默认使用SimpleAsyncUncaughtExceptionHandler,抛出的异常不会被程序捕获到;
  4. proxy-target-class,是否要创建CGLIB代理,默认是false,也就是创建的是基于Java接口的代理;
  5. mode,异步调用的模式,默认的异步调用是通过Spring AOP来实现的,不过我们可以通过该属性指定是否需要使用Aspectj的支持,使用Spring Aop代理时还可以通过proxy-target-class属性指定是否需要强制使用CGLIB做基于Class的代理;该属性有两个选项:proxyaspectj
1.2 task:executor标签

该标签是用于对任务执行的通用配置,可用于执行不同的任务策略:同步的,异步的,使用线程池的等。通常用于异步调用,来简单看下属性:

  1. id,这个不多说了,对应的executor的实例名称,通常是executor,也就是ThreadPoolTaskExecutor实例,一般用于Async异步执行的时候需要配置对应的executor;
  2. pool-size,线程池中线程的数量(单个值或者范围,如5-10);如果为10,表示核心线程数是10,最大线程数也是10;如果是5-10,表示核心线程数是5,最大线程数是10;如果不指定,则默认核心线程数是1,最大线程数是Integer.MAX_VALUE;最大线程数只有在队列容量不是无限制的时候才有用;
  3. queue-capacity,队列容量,如果没有指定,默认Integer.MAX_VALUE;
  4. keep-alive,表示超过核心线程数的线程 在完成任务之后,处于空闲状态的时间限制,也就是说过了这段时间之后,线程会终止掉,单位是秒;为0会导致多余的线程在执行完任务后立即终止,而不需要在任务队列中执行后续工作;
  1. rejection-policy,线程池中的任务队列满了以后对于新任务的处理策略:
    • ABORT 默认,抛出异常,然后不执行相应的任务;
    • DISCARD 不执行任务,也不抛出异常,也就是忽略这个任务;
    • DISCARD_OLDEST 将队列中最旧的那个任务丢弃,执行新任务;
    • CALLER_RUNS 不在新线程中执行任务,而是强制由调用者所在的线程来执行;
1.3 task:scheduler标签

该标签很简单,配置项不多,是用于定时任务相关的统一的配置,来看下属性:

  1. id,用于定时调度任务的ThreadPoolTaskScheduler的bean的id,一般用于@Scheduled定时任务执行;
  2. pool-size,定时调度线程池的大小,默认是1;
1.4 task:scheduled-tasks和task:scheduled标签

  task:scheduled-tasks标签及其子标签task:scheduled是用于配置具体的定时任务,该标签唯一的属性scheduler指定定时任务使用的scheduler实例,我们来看下task:scheduled标签的一些属性:

  1. ref,所引用的schedule的实例id;
  2. method,定时任务要调用的方法的名称;
  3. cron,cron表达式,这个就不多介绍了,网上有许多详细的介绍;
  4. fixed-delayfixed-rateinitial-delay,这三个属性在前篇文章中已经详细介绍过了,这里不多介绍了,文章地址[spring注解]Spring相关注解(四),然后ctrl + f,搜索scheduled字符串即可;
  5. trigger,实现触发器接口的bean;
1.5 简单总结

  到这,task标签的配置项已经介绍完了,这里的配置其实是一种半注解半XML的形式,上述配置项配置完之后,我们就可以使用注解AsyncScheduled来完成我们的异步调用和定时调度任务了。简单贴一下我们常用的配置形式:




2. 注解形式
2.1 具体实现

  上面的配置其实不是完全注解的形式,在上述配置中,我们可以通过task:scheduled标签来执行定时任务,也可以通过注解@Scheduled来执行定时任务。而这里,我们来看一下完全使用注解的形式:

  1. 在定时任务对象上添加两个用于开关的注解:@EnableScheduling(开启定时调度任务),@EnableAsync(开启异步执行);
  2. 定义用于定时调用的schedule实例和用于异步执行的executor实例,然后在需要调用的地方使用注解@Scheduled和@Async;

我们来简单看下实例:

@EnableWebMvc
@EnableScheduling
@EnableAsync
public class SpringConfig extends WebMvcConfigurerAdapter {

    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(5);
        return taskScheduler;
    }

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setMaxPoolSize(12);
        taskExecutor.setCorePoolSize(4);
        return taskExecutor;
    }
}

这里简单介绍下TaskExecutor

TaskExecutor,任务执行接口,可用于执行不同的任务策略:同步的,异步的,使用线程池的等;TaskExecutorjava.util.concurrent.Executor接口是相同的,实际上,它的存在主要是为了在使用线程池的时候,将对Java5的依赖抽象出来;该接口只有一个execute(Runnable task)方法,它根据线程池的语义和配置,来接受一个执行任务。

而针对执行任务的@Scheduled和@Async注解,在前文也已经介绍过了,这里就不再多介绍了,只简单说下@Async:

Async 注解用于类或者方法,用于标识某个方法或某个类的所有方法都是异步执行的,方法被调用的时候,会在新线程中执行,而调用它的方法会在原来的线程中执行;

2.2 其他说明
  1. TaskExecutor的实现类有许多,比如SimpleAsyncTaskExecutorSyncTaskExecutorConcurrentTaskExecutorSimpleThreadPoolTaskExecutorThreadPoolTaskExecutor等,而这里最常用的就是基于线程池的实现ThreadPoolTaskExecutor,有兴趣的可以看下它的常用参数,都很简单这里就不多说了,如果要了解更多,可以参考后面给出的官方文档地址。
  1. @Async注解标识的方法默认会被指定的TaskExecutor执行,如果需要针对某个特定的异步执行使用一个特定的TaskExecutor,则可以通过@Async的value属性指定需要使用的TaskExecutor对应的bean名称;至于该注解的其他属性,这里就不多说了。
2.3 注意事项

  使用@Async注解标识的方法进行异步调用是通过Spring AOP来实现的,所以@Async注解的方法必须是public方法,且必须是外部调用。这点其实和Transactional注解是类似的,也就是在同一个类中,一个方法调用另一个有注解的方法,注解是不会生效的。

3. Spring Task的集群问题

  使用Spring的注解@Scheduled可以很方便的执行一个定时任务,单台服务器下是没有问题的,如果在多台服务器做负载均衡的情况下,有可能会出现定时任务的重复执行的问题,因为集群的各台服务器之间数据是不会共享的,每台服务器上的任务都会按时执行。这种情况其实就是集群环境下的状态同步问题,针对这种情况,我们需要考虑的就是如何保证在同一时刻只有一个任务在执行。

3.1 借助数据库Mysql来实现

  由于数据库本身的锁机制,我们可以借助数据库来实现,其实也就是说两个任务同时操作表里的一条记录,只有一条能操作成功,我们可以借助数据库排他锁的这个特性来实现。

当然这种情况有一些问题需要考虑,比如说某台服务器挂了数据库锁的释放,表中记录状态的维护等。

3.2 基于redis实现

  其实这个问题就是一个任务互斥的问题,如果学过操作系统的话,就很好理解,就是操作系统中所谓的PV操作,也就是说,声明一个分布式互斥锁,一台服务器获得锁后,改变锁状态,然后执行任务,任务完成还原状态;另一台服务器获取锁,如果是锁定状态,说明正在有其他服务器执行,不执行任何操作,直接结束。

  1. 我们可以借助Redis的原子特性,使用递增递减方法来实现,如果使用jedis来操作的话,使用incr和decr来实现;
  2. 无论任务是否执行成功,一定要记得执行状态的还原,也就是锁失效状态,这种可以通过指定超时时间来实现;同样,如果服务器重启,记得重置该锁的状态;
  3. 超时时间的设置不要大于两次任务间隔的时间;

简单写下代码:

String cacheKeyPV = ...;

@PostConstruct
public void init() {
    // 封装方法,设置超时时间等
    jedis.incr(cacheKeyPV);
}

try {
    //P:设置为1
    // 借助jedis的expire等方法简单封装一下incr
    long resources = jedis.incr(cacheKeyPV,10, CacheTimeUnit.SECOND);
    if(resources == 1) {
        // 执行操作
    }
} catch (Exception e) {
    // exception
} finally {
    try {
        //V:重新设置为0
        jedis.decr(cacheKeyPV) ;
    } catch (Exception e) {
        // catch
    }
}
3.3 Quartz框架

  Quartz框架是一个优秀的框架,功能十分强大,支持集群环境下的任务调度,但功能有点复杂,对于一些常规的简单的项目,是不建议使用的,如果有兴趣的,可以了解下。

3.4 借助一些开源的分布式任务调度系统

  目前,市面上有许多开源的分布式的任务调度系统,比如说LTS,Elastic-Job,Uncode-Schedule等,如果需要了解更多,可以参考:https://my.oschina.net/editorial-story/blog/883856

参考地址:
官方文档:Spring4.3.15.Release-docs-html-scheduling
官方文档的中文翻译可参考的一篇文章:https://www.jianshu.com/p/69e44b93bb47
Spring API地址:https://docs.spring.io/spring/docs/
其他参考自:在同一个类中,一个方法调用另外一个有注解方法失败的原因
另外,刚兴趣的童鞋可以再去看下操作系统的PV操作,这个很有意思的。

你可能感兴趣的:([spring]task标签及定时任务相关)