1、cron表达式
语法:秒 分 时 日 月 周 年(spring不支持)
http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
实例:
在线生成cron表达式:https://cron.qqe2.com/
2、Springboot整合定时任务和异步任务
定时任务
1、开启定时任务@EnableScheduling
2、@Scheduled 开启一个定时任务
3、自动配置类TaskSchedulingAutoConfiguration
4、定时任务会开启一条线程A去执行,就算有多个定时任务,也是使用这个线程A去一个一个执行。开启@EnableAsync @Async异步任务的话。就能够使用默认的线程池,来执行定时任务了
异步任务
1、@EnableAsync开启异步任务
2、@Async给要异步执行的方法标上注解
3、自动配置类TaskExecutionAutoConfiguration
属性绑定在TaskExecutionProperties
底层也是有用到线程池的,默认核心线程数是8,大概看了一下自动配置类,应该是如果系统中没有自定义的线程池,就创建一个新的线程池,如果已经有了,就直接用自定义的线程池
示例:
@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class HelloSchedule {
/**
*1、在spring中cron6位组成,不允许第七位年
*2、在周的位置,1-7代表周一到周日或MON-SUN
*3、定时任务不应该阻塞。默认是阻塞的
* 3.1、可以让业务以异步的方式运行,自己提交到线程池
* 3.2、使用定时任务自带的线程池
* 定时任务自带线程池的线程数:spring.task.scheduling.pool.size=5
* 有些springboot版本设置了这个也不好使,可能是bug
* 3.3、让定时任务异步执行,直接让整个定时任务方法异步执行,而不是方法里的业务异步执行
* springboot提供了异步任务的功能,除了我们自己写一个线程池,把我们自己要执行的
* 业务丢给线程池执行之外,springboot还支持异步任务
* 一、类上加注解@EnableAsync,开启异步任务
* 二、给要异步执行的方法标上注解@Async
*
*
*
*/
@Async
@Scheduled(cron = "* * * ? * 3")
public void hello() throws InterruptedException {
log.info("hello...");
//模拟阻塞,业务执行时间很长
Thread.sleep(3000);
}
}
配置:可以参考TaskExecutionProperties来进行配置
##异步任务设置
spring.task.scheduling.pool.size=5
spring.task.execution.pool.max-size=50
3、分布式下定时任务的问题
三台机器A1,A2,A3,都有一个定时任务,同一段程序,定时任务的设置都一样,等时间一到,它们就都同时启动了定时任务,就要同时执行业务代码,就会出现幂等性问题
解决:
应该是定时任务,只能有一台机器在执行,不能所有机器同时执行。
使用分布式锁解决。
@Slf4j
@Service
public class seckillSkuScheduled {
@Autowired
private SeckillService seckillService;
@Autowired
private RedissonClient redissionClient;
private final String upload_lock="seckill:upload:lock";
//保证幂等性问题
@Scheduled(cron = "*/5 * * * * ?")
public void uploadSeckillSkuLatest3Days() {
log.info("上架秒杀的商品");
//分布式锁
RLock lock = redissionClient.getLock(upload_lock);
//锁的超时时间
lock.lock(10, TimeUnit.SECONDS);
try{
seckillService.uploadSeckillSkuLatest3Days();
}finally {
//释放锁
lock.unlock();
}
}
}
4、SchedulingConfigurer接口定义定时任务
springboot
定时任务除了使用注解的方式之外,还能同时实现接口SchedulingConfigurer
来定义定时任务。
例如
@Slf4j
public class UpdateUserScheduleTask extends AbstractUpdateUserExpireTimeTask implements UpdateUserExpireTimeTask , SchedulingConfigurer {
//常用cron表达式
//10秒执行一次:*/10 * * * * ?
//每天凌晨两点执行一次:0 0 2 * * ?
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.addCronTask(()->{
execute();
},"*/5 * * * * ?");
log.info("定时任务注册器加载完成");
}
}
配置类:
@EnableScheduling
@Bean
@ConditionalOnMissingBean
public UpdateUserExpireTimeTask updateUserExpireTimeTask(){
return new UpdateUserScheduleTask();
}
自定义接口:
public interface UpdateUserExpireTimeTask {
void execute();
}
抽象类:
@Slf4j
public class AbstractUpdateUserExpireTimeTask implements UpdateUserExpireTimeTask{
@Autowired
private UserService userService;
@Override
public void execute() {
long startTime = System.currentTimeMillis();
log.debug("开始执行任务---->更新用户是否过期-->startTime:" + startTime);
LocalDateTime now = LocalDateTime.now().with(LocalTime.MIN);
//开始生效的用户
userService.updateIsExpired(ExpireTypeEnum.UNEXPIRED,ExpireTypeEnum.EXPIRED,now,null);
//开始过期的用户
userService.updateIsExpired(ExpireTypeEnum.EXPIRED,ExpireTypeEnum.UNEXPIRED,null,now);
log.debug("任务执行结束,共耗时:" + (System.currentTimeMillis() - startTime) + "毫秒");
}
}
按道理来说,我定时任务直接实现接口SchedulingConfigurer
,就行了,为啥还需要接口UpdateUserExpireTimeTask
?
1、其实UpdateUserExpireTimeTask
可以用来做一个标记,因为如果有两个定时任务都实现了接口SchedulingConfigurer
,在配置类中注入的时候就会有冲突因此,通过自定义一个接口来区别
2、控制该定时任务是可以执行,还是不让执行,也就是控制它的开关,为什么说能控制它的开关呢?因为如果我不想使用springboot
的定时任务,我想使用第三方的定时任务框架,去做定时任务,那么我只需要将定时任务去实现接口UpdateUserExpireTimeTask
以及第三方定时任务提供的接口就行,然后我在配置类中注入该定时任务UpdateUserExpireTimeTask
,这样就能覆盖掉之前定义的Spring boot
的定时任务,因为标注了@ConditionalOnMissingBean
为什么中间加一个抽象类AbstractUpdateUserExpireTimeTask
呢?
因为加一个抽象层,能把多个子类中相同的代码,抽出来放到里面去,当然如果不需要注入外部资源,直接写到接口里,作为默认实现也是可以的