一、 前言
本篇文章主要分为以下三部分:
- 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命名空间中最基础的标签,用于开启定时任务和异步调用的注解支持,来简单看下该注解的几个属性:
- scheduler,配置对应的定时任务对象,如果没有配置,默认将使用TaskScheduler实例;
- executor,配置对应的异步执行的对象,如果没有配置,默认将使用SimpleAsyncTaskExecutor实例;
- exception-handler,在异步执行期间,抛出的异常的实例,默认使用SimpleAsyncUncaughtExceptionHandler,抛出的异常不会被程序捕获到;
- proxy-target-class,是否要创建CGLIB代理,默认是false,也就是创建的是基于Java接口的代理;
- mode,异步调用的模式,默认的异步调用是通过Spring AOP来实现的,不过我们可以通过该属性指定是否需要使用Aspectj的支持,使用Spring Aop代理时还可以通过proxy-target-class属性指定是否需要强制使用CGLIB做基于Class的代理;该属性有两个选项:
proxy
,aspectj
;
1.2 task:executor标签
该标签是用于对任务执行的通用配置,可用于执行不同的任务策略:同步的,异步的,使用线程池的等。通常用于异步调用,来简单看下属性:
- id,这个不多说了,对应的executor的实例名称,通常是executor,也就是ThreadPoolTaskExecutor实例,一般用于Async异步执行的时候需要配置对应的executor;
- pool-size,线程池中线程的数量(单个值或者范围,如5-10);如果为10,表示核心线程数是10,最大线程数也是10;如果是5-10,表示核心线程数是5,最大线程数是10;如果不指定,则默认核心线程数是1,最大线程数是Integer.MAX_VALUE;最大线程数只有在队列容量不是无限制的时候才有用;
- queue-capacity,队列容量,如果没有指定,默认Integer.MAX_VALUE;
- keep-alive,表示超过核心线程数的线程 在完成任务之后,处于空闲状态的时间限制,也就是说过了这段时间之后,线程会终止掉,单位是秒;为0会导致多余的线程在执行完任务后立即终止,而不需要在任务队列中执行后续工作;
- rejection-policy,线程池中的任务队列满了以后对于新任务的处理策略:
ABORT
默认,抛出异常,然后不执行相应的任务;DISCARD
不执行任务,也不抛出异常,也就是忽略这个任务;DISCARD_OLDEST
将队列中最旧的那个任务丢弃,执行新任务;CALLER_RUNS
不在新线程中执行任务,而是强制由调用者所在的线程来执行;
1.3 task:scheduler标签
该标签很简单,配置项不多,是用于定时任务相关的统一的配置,来看下属性:
- id,用于定时调度任务的ThreadPoolTaskScheduler的bean的id,一般用于@Scheduled定时任务执行;
- pool-size,定时调度线程池的大小,默认是1;
1.4 task:scheduled-tasks和task:scheduled标签
task:scheduled-tasks
标签及其子标签task:scheduled
是用于配置具体的定时任务,该标签唯一的属性scheduler
指定定时任务使用的scheduler实例,我们来看下task:scheduled
标签的一些属性:
- ref,所引用的schedule的实例id;
- method,定时任务要调用的方法的名称;
- cron,cron表达式,这个就不多介绍了,网上有许多详细的介绍;
- fixed-delay,fixed-rate,initial-delay,这三个属性在前篇文章中已经详细介绍过了,这里不多介绍了,文章地址[spring注解]Spring相关注解(四),然后ctrl + f,搜索
scheduled
字符串即可;- trigger,实现触发器接口的bean;
1.5 简单总结
到这,task标签的配置项已经介绍完了,这里的配置其实是一种半注解半XML的形式,上述配置项配置完之后,我们就可以使用注解Async
和Scheduled
来完成我们的异步调用和定时调度任务了。简单贴一下我们常用的配置形式:
<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
<task:executor id="myExecutor" pool-size="5"/>
<task:scheduler id="myScheduler" pool-size="10"/>
2. 注解形式
2.1 具体实现
上面的配置其实不是完全注解的形式,在上述配置中,我们可以通过task:scheduled
标签来执行定时任务,也可以通过注解@Scheduled来执行定时任务。而这里,我们来看一下完全使用注解的形式:
- 在定时任务对象上添加两个用于开关的注解:
@EnableScheduling
(开启定时调度任务),@EnableAsync
(开启异步执行);- 定义用于定时调用的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
,任务执行接口,可用于执行不同的任务策略:同步的,异步的,使用线程池的等;TaskExecutor
和java.util.concurrent.Executor
接口是相同的,实际上,它的存在主要是为了在使用线程池的时候,将对Java5的依赖抽象出来;该接口只有一个execute(Runnable task)方法,它根据线程池的语义和配置,来接受一个执行任务。
而针对执行任务的@Scheduled和@Async注解,在前文也已经介绍过了,这里就不再多介绍了,只简单说下@Async:
Async 注解用于类或者方法,用于标识某个方法或某个类的所有方法都是异步执行的,方法被调用的时候,会在新线程中执行,而调用它的方法会在原来的线程中执行;
2.2 其他说明
- TaskExecutor的实现类有许多,比如
SimpleAsyncTaskExecutor
,SyncTaskExecutor
,ConcurrentTaskExecutor
,SimpleThreadPoolTaskExecutor
,ThreadPoolTaskExecutor
等,而这里最常用的就是基于线程池的实现ThreadPoolTaskExecutor,有兴趣的可以看下它的常用参数,都很简单这里就不多说了,如果要了解更多,可以参考后面给出的官方文档地址。
- @Async注解标识的方法默认会被<task:annotation-driven/>指定的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操作,也就是说,声明一个分布式互斥锁,一台服务器获得锁后,改变锁状态,然后执行任务,任务完成还原状态;另一台服务器获取锁,如果是锁定状态,说明正在有其他服务器执行,不执行任何操作,直接结束。
- 我们可以借助Redis的原子特性,使用递增递减方法来实现,如果使用jedis来操作的话,使用incr和decr来实现;
- 无论任务是否执行成功,一定要记得执行状态的还原,也就是锁失效状态,这种可以通过指定超时时间来实现;同样,如果服务器重启,记得重置该锁的状态;
- 超时时间的设置不要大于两次任务间隔的时间;
简单写下代码:
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操作,这个很有意思的。