一.基本使用
使用前记得在Spring
启动类中开启定时任务。
@EnableAsync
@scheduled
注解支持不同方式的任务调度。
1.cron表达式
当方法的执行时间超过任务调度频率时,调度器会在下个周期执行。 例如:任务每3s执行一次,执行4s,则假设任务在第0s开始执行,下一次执行时间是第6s。
2.fixedRate
fixedRate
是按照一定的速率执行,是从上一次方法执行开始的时间算起,如果上一次方法阻塞住了,下一次也是不会执行,但是在阻塞这段时间内累计应该执行的次数,当不再阻塞时,一下子把这些全部执行掉,而后再按照固定速率继续执行。
3.fixedDelay
fixedDelay
控制方法执行的间隔时间,是以上一次方法执行完开始算起,如上一次方法执行阻塞住了,那么直到上一次执行完,并间隔给定的时间后,执行下一次。
二.线程配置
在实际项目中,一个应用实例中可能会使用@Scheduled
会定义多个任务,在默认情况下,多个任务会共享同一个线程,当有一个任务阻塞时,所有的任务都无法得到执行。所以当存在多个任务时,需要做任务的线程配置。
1.任务内使用统一线程,任务间使用不同线程
每个定时任务会占用1个线程。但是相同的定时任务,执行的时候,还是在同一个线程中。例如:任务1和任务2开始运行,任务1卡死了,任务2正常运行结束,在下一个周期中,任务1继续卡死,需要等待上一次任务结束才会启动新的任务,而任务2正常运行。
1)实现SchedulingConfigurer接口。重写configureTasks方法
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(50));
}
}
2)配置任务线程池
注意:
此时任务方法上无需添加@Async注解。
@Configuration
@EnableAsync
public class ScheduleConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(50);
return taskScheduler;
}
}
2.任务内和任务间都使用不同线程
如上所示,首先配置任务线程池,同时任务方法上需要添加@Async
注解。此时每次运行一个任务,都会启动一个线程来运行,也就是说同一个任务也会启动多个线程。
例如:任务1和任务2一起执行,任务1卡住了,任务2正常运行结束,下一个周期,任务1会继续启动执行。但是任务1中的卡死线程越来越多,会导致50个线程池占满,还是会影响到定时任务。
@Async
@Scheduled(cron = "0/10 * * * * *")
public void task() { }
三.分布式任务锁
当应用以多实例的方式部署时,对于同一个任务,为了保证在同一时间,只有一个实例里的任务在运行,需要使用@SchedulerLock(name = "xxxx")
。ShedLock
的实现原理是采用公共存储实现的锁机制,使得同一时间点只有第一个执行定时任务的服务实例能执行成功,并在公共存储中存储"我正在执行任务,从什么时候(预计)执行到什么时候",其他服务实例执行时如果发现任务正在执行,则直接跳过本次执行,从而保证同一时间一个任务只被执行一次。SchedLock
目前支持的公共存储有:
- Monogo
- DynamoDB
- JdbcTemplate
- ZooKeeper
- Redis
- Hazelcast
笔者目前项目中使用的是JdbcTemplate
,使用步骤如下:
1.引入依赖
在spring-boot
项目中引入maven依赖。
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>2.2.0</version>
</dependency>
2.在Spring启动类中开启shedlock
@EnableSchedulerLock(defaultLockAtMostFor = "PT20M")
defaultLockAtMostFor
表示成功获取锁,执行任务的节点所能拥有的独占锁的最长时间的字符串表达,该字段是为了防止获取锁的节点在执行任务时死掉,长时间不释放锁,该字段的值应大于任务正常运行的正常时间。如果获取锁执行任务的节点正常运行完任务,那么锁会被立刻释放。
3.新增shedlock配置类
@Configuration
public class ShedlockConfig {
@Resource
private DataSource dataSource;
@Bean
public LockProvider lockProvider() {
return new JdbcTemplateLockProvider(dataSource);
}
}
笔者的项目使用mysql作为数据库,dataSource
表示mysql数据源,需要在mysql数据库中新建名为shedlock
的表,作为分布式锁。建表语句如下:
CREATE TABLE shedlock(
name VARCHAR(64) , -- 锁名称 ,name必须是主键
lock_until TIMESTAMP(3) NULL, -- 释放锁时间
locked_at TIMESTAMP(3) NULL, -- 获取锁时间
locked_by VARCHAR(255), -- 上锁的应用实例
PRIMARY KEY (name)
)
4.在定时任务上加锁
在使用@Schedule
的定时任务上新增shedlock锁。
@SchedulerLock(name = "锁的名字")
name
对应shedlock表中的name字段,注意:不同的定时任务不能重名。Spring应用启动后,应用实例获取到锁,会自动在数据库中insert
数据,所以并不需要手动初始化数据。
5.获取锁/释放锁参数设置
shedlock
锁主要涉及参数有:lockAtMostFor
:锁的最大时间单位为毫秒lockAtMostForString
:最大时间的字符串形式,例如:PT30S 代表30秒lockAtLeastFor
:锁的最小时间单位为毫秒lockAtLeastForString
:最小时间的字符串形式
汇总下来,主要是两个参数:最大锁定时间和最小锁定时间,接下来笔者将从一个实例获取到锁为例,梳理一下这两个参数是如何作用的。
1)实例获取到锁,将locked_at
设置为当前时间,将lock_until
设置为当前时间+最大锁定时间;
2)实例运行完成定时任务后,更新lock_until
字段为locked_at
+max(最小锁定时间,实例运行任务时间)
,例如:最小锁定时间30s,实例获取锁时间只过了15s,那么就会取最小锁时间30s;否则用当前时间。这个就是为了保证最少也要锁最小锁定时间30s。如果实例死掉或者一直没有运行完,则lock_until
不变;
3)当lock_until小于等于当前时间,表明原先实例已释放锁,其他实例可以通过sql来重新抢锁了
UPDATE tableName SET lock_until = 当前时间+最大锁定时间, locked_at = 当前时间, locked_by = 主机名 WHERE name = 锁名字 AND lock_until <= 当前时间
sql运行成功,表明该实例获取锁成功;
综上所述,最大锁定时间需要设置的小于定时任务的最长运行时间,该字段的设置也用于防止程序无法正常释放锁导致死锁。而最小锁定时间一般设置的很小,例如5-10s即可,用于解决各个实例间时钟不同步的问题。