定时任务的实现方式有多种,例如JDK自带的Timer+TimerTask方式,Spring 3.0以后的调度任务(Scheduled Task),Quartz等。
因为项目中用到了Scheduled,所以这里只说Scheduled。
1. SpringBoot启动类上加注解
@EnableScheduling
2. 自定义线程池。
spring底层默认是new一个核心数量为1的单线程池,如果需要对定时器线程池核心线程数量调优或自定义什么的,可以新增一个配置类,实现SchedulingConfigurer接口,重写configureTasks方法,通过taskRegistrar设置自定义线程池。
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
(taskExecutor());
}
@Bean(destroyMethod="shutdown")
public Executor taskExecutor() {
return (20);
}
}
3. 用法:实现一个基本的调度方法。基本结构如下:
package ;
import ;
import ;
@Service
public class ScheduledService {
@Scheduled(cron = "0 0 5 * * *")
public void build() {
("Scheduled Task");
}
}
@Scheduled注解支持秒级的cron表达式,上述声明表示每天5点执行build任务,当然本篇重点不是介绍cron,想知道更多的cron表达式,可以看我的另一篇博文 @Scheduled注解 详解,里面有详细语法介绍。
好,回到正题,前文已经提过,这种方式在单台应用服务器上运行没有问题,但是在集群环境下,会造成build任务在5点的时候运行多次,遗憾的是,Scheduled Task在框架层面没有相应的解决方案,只能靠程序员在应用级别进行控制。
如何控制?
1. 无非是一个任务互斥访问的问题,声明一把全局的“锁”作为互斥量,哪个应用服务器拿到这把“锁”,就有执行任务的权利,未拿到“锁”的应用服务器不进行任何任务相关的操作。
2.这把“锁”最好还能在下次任务执行时间点前失效。
在项目中我将这个互斥量放在了redis缓存里,9分钟过期,这个过期时间是由任务调度的间隔时间决定的,只要小于两次任务执行时间差,大于集群间应用服务器的时间差即可。
完整定时任务类如下,该段代码支持多台机子部署,不会出现多台服务都同时执行的情况,当然前提是他们用的redis都是同一个:
@Component
public class AutoInsertVuserToGroupBuying {
@Autowired
private RedisTemplate redisTemplate;
@Scheduled(cron = "0 */10 * * * ?") //定时器10分钟一次
public void shTask() {
//先判断redis中是否有锁记录,如果能设值成功,代表拿到锁,不能设值成功就是锁还没释放
if(().setIfAbsent(key, value)){
//设值成功后,设置锁超时时间 (我这里是9分钟)
(key, 9, );
//业务
dojob();
}
}
}
PS: 很多人说9到11行之间会存在并发问题,其实并不会,因为setIfAbsent先天自带锁,是基于redis的setnx来保证原子性,像这里是不可能存在两个线程同时设值成功的, 只有设值成功的才能拿到锁,没设值成功的就拿不到锁.