springboot】--任务调度@Scheduled、ScheduledThreadPoolExecutor、quartz、xxl-job

目录
一、springboot集成@Scheduled注解
1.1、集成@Scheduled注解方法
1.2、集成@Scheduled注解优劣点
1.3、集成@Scheduled注解改进
1.5、@Scheduled注解+@Async注解 能否解决单线程问题
1.6、集成@Scheduled注解优缺点汇总
二、springboot使用ScheduledThreadPoolExecutor定时调度
2.1、ScheduleAtFixedRate方法
2.2、ScheduleWithFixedDelay方法
三、springboot集成quartz
3.1、简单实现
3.2、quartz优缺点
四、springboot集成xxl-job
4.1、编译启动xxl-job-admin
4.2、springboot集成xxl-job
五、任务调度技术的选择
说明
定时任务调度在很多场景中应用,并且市场上也有很多技术栈(如@Scheduled、ScheduledThreadPoolExecutor、quartz等),下面就以Springboot集成这些定时任务调度技术进行对比,比较在实际应用中的优劣。

一、springboot集成@Scheduled注解

1.1、集成@Scheduled注解方法
要想开启@Scheduled,只需要Application启动类增加@EnableScheduling注解即可。

@SpringBootApplication(scanBasePackages= {"com.wwy"})
@EnableScheduling //开启定时任务功能
public class LearnitemApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder){
return builder.sources(LearnitemApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(LearnitemApplication.class, args);
}
}

具体使用过程

@Component
public class ScheduleTaskServer {

/**
* cron 每10秒执行
*/
@Scheduled(cron = "0/1 * * * * *")
public void cronV1(){
System.out.println("cronV1 "+Thread.currentThread().getName()+" 执行时间start:"+new Date());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Scheduled(cron = "0/1 * * * * *")
public void cronV2(){
System.out.println("cronV2 "+Thread.currentThread().getName()+" 执行时间start:"+new Date());
}
}

结果:

* cronV2 scheduling-1 执行时间start:Mon Jan 10 11:18:35 CST 2022
* cronV1 scheduling-1 执行时间start:Mon Jan 10 11:18:35 CST 2022
* cronV2 scheduling-1 执行时间start:Mon Jan 10 11:18:40 CST 2022
* cronV1 scheduling-1 执行时间start:Mon Jan 10 11:18:41 CST 2022
* cronV2 scheduling-1 执行时间start:Mon Jan 10 11:18:46 CST 2022
* cronV1 scheduling-1 执行时间start:Mon Jan 10 11:18:47 CST 2022
* cronV2 scheduling-1 执行时间start:Mon Jan 10 11:18:52 CST 2022
* cronV1 scheduling-1 执行时间start:Mon Jan 10 11:18:53 CST 2022

结果分析:
即使开启两个@Scheduled,也是同一个线程【scheduling-1】执行,说明@Scheduled注解是单线程。
本身 cronV2先执行完,然后等待cronV1执行完,才能执行cronV2。

1.2、集成@Scheduled注解优劣点
存在问题:
(1)、单线程执行,如果某个任务阻塞会影响其他任务;
(2)、某个任务失败,后续还会继续执行,并且任务随着系统服务启动而开启,不能终止或中途添加;
(3)、只能适用于单机服务架构,不适合分布式场景;

1.3、集成@Scheduled注解改进
针对上面第一个存在问题,可以使用多线程来解决。--------------采用多线程解决方法有两种:(1)、通过配置设置Scheduled为多线程;(2)、自己维护线程池。

通过配置设置Scheduled为多线程

@Configuration
public class ScheduleConfiguration implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
}
}

自己维护线程池

@Component
public class AsyncScheduleTaskServer {

private static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
2,2,1000, TimeUnit.SECONDS,new LinkedBlockingDeque<>(100));

@Scheduled(cron = "0/1 * * * * *")
public void asyncCronV1(){
poolExecutor.execute(()->{
System.out.println("async CronV1 "+Thread.currentThread().getName()+" 执行时间start:"+new Date());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}

@Scheduled(cron = "0/1 * * * * *")
public void asyncCronV2(){
poolExecutor.execute(()->{
System.out.println("async CronV2 "+Thread.currentThread().getName()+" 执行时间start:"+new Date());
});
}
}

总结说明:
线程的coreSize数设置不够,那么任务也会在队列中排列等待执行。

1.5、@Scheduled注解+@Async注解 能否解决单线程问题
@Async注解是异步执行,@Scheduled注解是单线程执行,所以,即使加上@Async注解也不能解决单线程问题。

具体代码验证

@Scheduled(cron = "0/4 * * * * *")
public void cronV1(){
System.out.println("cronV1 "+Thread.currentThread().getName()+" 执行时间start:"+new Date());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("cronV1 "+Thread.currentThread().getName()+" 执行时间end:"+new Date());
}

@Scheduled(cron = "0/5 * * * * *")
public void cronV2(){
System.out.println("cronV2 "+Thread.currentThread().getName()+" 执行时间start:"+new Date());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("cronV2 "+Thread.currentThread().getName()+" 执行时间end:"+new Date());
}

打印结果

cronV1 scheduling-1 执行时间start:Sun Jul 03 16:57:24 CST 2022
cronV1 scheduling-1 执行时间end:Sun Jul 03 16:57:29 CST 2022
cronV2 scheduling-1 执行时间start:Sun Jul 03 16:57:29 CST 2022
cronV2 scheduling-1 执行时间end:Sun Jul 03 16:57:30 CST 2022
cronV1 scheduling-1 执行时间start:Sun Jul 03 16:57:32 CST 2022
cronV1 scheduling-1 执行时间end:Sun Jul 03 16:57:37 CST 2022
cronV2 scheduling-1 执行时间start:Sun Jul 03 16:57:37 CST 2022
cronV2 scheduling-1 执行时间end:Sun Jul 03 16:57:38 CST 2022
cronV2 scheduling-1 执行时间start:Sun Jul 03 16:57:40 CST 2022
cronV2 scheduling-1 执行时间end:Sun Jul 03 16:57:41 CST 2022
cronV1 scheduling-1 执行时间start:Sun Jul 03 16:57:41 CST 2022
cronV1 scheduling-1 执行时间end:Sun Jul 03 16:57:46 CST 2022
cronV2 scheduling-1 执行时间start:Sun Jul 03 16:57:46 CST 2022
cronV2 scheduling-1 执行时间end:Sun Jul 03 16:57:47 CST 2022
cronV1 scheduling-1 执行时间start:Sun Jul 03 16:57:48 CST 2022
cronV1 scheduling-1 执行时间end:Sun Jul 03 16:57:53 CST 2022

 

 


-----cronV1是每4S执行一次,cronV1执行花费5秒;cronV2是每5秒执行一次,cronV2执行花费1秒。
从打印结果上看,利用一个图形更好的解释:

首先,cron1和cron2两个任务,谁先启动时没要求,随机的串行执行;
任意一个任务执行了,执行结束后,其他任务依次执行;
当一个任务(id=1)正在执行,没在间隔时间内执行完,当该任务(id=1)发现自身任务还在执行,那么不进行其他处理,等待下一次间隔时间;
当一个任务(id=1)正在执行,任务(id=2)间隔时间到了,发现其他任务在执行,那么相当于排队一样,其他任务执行完,立马执行自身;
如果某个时间段没任何任务在执行,当某个任务的间隔时间到了,就哪个任务执行。

总结:
@Scheduled注解+@Async注解的打印也是一样,所以说不能解决单线程串行执行的能力。

1.6、集成@Scheduled注解优缺点汇总
存在问题:
(1)、单线程执行,如果某个任务阻塞会影响其他任务;
-----多线程解决。但是,如何设置coreSize需要调整,否则还是有任务阻塞等待。
(2)、某个任务失败,后续还会继续执行,并且任务随着系统服务启动而开启,不能终止或中途添加;
(3)、只能适用于单机服务架构,不适合分布式场景;

二、springboot使用ScheduledThreadPoolExecutor定时调度

思考:
@Scheduled是定时任务,服务器节点启动后就会按照固定时间点/时间间隔执行,即使上一个任务没执行完,后续到时间点,继续执行。
那么对于随机时刻触发场景【如订单流转】,那么需要考虑ScheduledThreadPoolExecutor来解决。

2.1、ScheduleAtFixedRate方法

public class ScheduledThreadPoolServer {
//开启一个定时任务线程池
private static ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(5);
//存放正在使用的定时任务
private static ConcurrentHashMap<String,Future> futureMap = new ConcurrentHashMap<>();
public Integer addTask(final String key,final String value){
//如果任务已经存在线程池,那么不重复添加
if(futureMap.contains(key)) return -1;
/**
* initialDelay 表示初始化延迟
* period 表示两次执行最小时间间隔
*/
Future future = scheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
//执行具体的任务功能
try {
System.out.println("任务: "+key+" 线程id: "+Thread.currentThread().getName()+" 时间start:"+new Date());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},0,5, TimeUnit.SECONDS);
//任务放入Map
if(!futureMap.contains(key)) futureMap.put(key,future);
return 1;
}

public void removeTask(String key){
Future future = futureMap.get(key);
//直接取消线程池中的任务
future.cancel(true);
futureMap.remove(key);
}
}

2.2、ScheduleWithFixedDelay方法

public Integer addTaskV2(final String key,final String value){
//如果任务已经存在线程池,那么不重复添加
if(futureMap.contains(key)) return -1;
/**
* initialDelay 首次执行的延迟时间
* delay 一次执行终止和下一次执行开始之间的延迟
*
* -----和scheduleAtFixedRate区别:是任务上一次执行完后,才开始计算延迟。
*/
Future future = scheduler.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
//执行具体的任务功能
try {
System.out.println("FixedDelay任务: "+key+" 线程id: "+Thread.currentThread().getName()+" 时间start:"+new Date());

} catch (InterruptedException e) {
e.printStackTrace();
}
}
},0,5,TimeUnit.SECONDS);
//任务放入Map
if(!futureMap.contains(key)) futureMap.put(key,future);
return 1;
}

和scheduleAtFixedRate区别:是任务上一次执行完后,才开始计算延迟。

三、springboot集成quartz

思考:
ScheduledThreadPoolExecutor功能能满足大部分场景,但是也存在明显的不足:(1)、任务有异常,不能单独暂停该任务;(2)、调度cron时间配置不灵活;(3)、不能灵活开启关闭任务。

----------针对上面的问题,可以考虑比较大众化使用的quartz任务调度【它的执行过程类似的ScheduledThreadPoolExecutor 的scheduleAtFixedRate方法,但是可以随时开启暂停任务、调整调度时间频率。】。

3.1、简单实现
Pom.xml文件

<!-- quartz依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

要执行的任务

@Component
public class TestV1Job implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("TestV1Job 任务,定时执行start: "+new Date());
}
}

任务处理功能【添加、暂停、修改、重新执行等操作】

@Component
public class JobHandler {
Logger log = LoggerFactory.getLogger(JobHandler.class);
@Resource
private Scheduler scheduler;

/**
* 添加任务
*/
public void addJob(JobInfo jobInfo) throws SchedulerException, ClassNotFoundException {
//生成job key
JobKey jobKey = JobKey.jobKey(jobInfo.getJobName(), jobInfo.getJobGroup());
//已经存在的job,需要进一步判断
if (scheduler.checkExists(jobKey)) {
log.warn("JobHandler add Job 已经存在 jobInfo:" + JSON.toJSONString(jobInfo));
return;
//具体的添加job且调度器信息
addDetail(jobInfo);
}

public void addDetail (JobInfo jobInfo) throws ClassNotFoundException, SchedulerException {
JobKey jobKey = JobKey.jobKey(jobInfo.getJobName(), jobInfo.getJobGroup());
Class<Job> jobClass = (Class<Job>) Class.forName(jobInfo.getClassName());
// 任务明细
JobDetail jobDetail = JobBuilder
.newJob(jobClass)
.withIdentity(jobKey)
.withIdentity(jobInfo.getJobName(), jobInfo.getJobGroup())
.withDescription(jobInfo.getJobName())
.build();
// 配置信息
jobDetail.getJobDataMap().put("config", jobInfo.getConfig());
// 定义触发器
TriggerKey triggerKey = TriggerKey.triggerKey(jobInfo.getTriggerName(), jobInfo.getTriggerGroup());
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(triggerKey)
.withSchedule(CronScheduleBuilder.cronSchedule(jobInfo.getCron()))
.build();
scheduler.scheduleJob(jobDetail, trigger);
}

/**
* 任务暂停
*/
public void pauseJob (String jobGroup, String jobName) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
if (scheduler.checkExists(jobKey)) {
scheduler.pauseJob(jobKey);
}
}

/**
* 修改任务的 cron表达式
*/
public void modifyJob (String cron, String jobGroup, String jobName, String triGroup, String triName) throws
SchedulerException {
//新的调度器
CronTrigger newTrigger = TriggerBuilder.newTrigger().withIdentity(triName, triGroup)
.withSchedule(CronScheduleBuilder.cronSchedule(cron)).build();
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
if (scheduler.checkExists(jobKey)) {
scheduler.rescheduleJob(TriggerKey.triggerKey(triName, triGroup), newTrigger);
}
}

/**
* 删除任务
*/
public boolean deleteJob (String jobGroup, String jobName) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
if (scheduler.checkExists(jobKey)) {
return scheduler.deleteJob(jobKey);
}
return false;
}

/**
* 获取任务信息
*/
public List<JobInfo> getJobInfo (String jobGroup, String jobName) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
if (!scheduler.checkExists(jobKey)) {
return null;
}
List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
if (Objects.isNull(triggers)) {
throw new SchedulerException("未获取到触发器信息");
}
List<JobInfo> jobList = new ArrayList<>();
for (Trigger temp : triggers) {
JobInfo jobInfo = new JobInfo();
jobInfo.setJobName(jobGroup);
jobInfo.setJobGroup(jobName);
jobInfo.setTriggerName(temp.getKey().getName());
jobInfo.setTriggerGroup(temp.getKey().getGroup());

JobDetail jobDetail = scheduler.getJobDetail(jobKey);
jobInfo.setClassName(jobDetail.getJobClass().getName());
if (Objects.nonNull(jobDetail.getJobDataMap())) {
jobInfo.setConfig(JSON.toJSONString(jobDetail.getJobDataMap()));
}
Trigger.TriggerState triggerState = scheduler.getTriggerState(temp.getKey());
jobInfo.setStatus(triggerState.toString());
jobInfo.setCron(((CronTrigger) temp).getCronExpression());
jobList.add(jobInfo);
}
return jobList;
}
}
}

3.2、quartz优缺点
测试分析得到结果:
(1)、适合单机服务,可以使用任务启动/暂停/恢复/删除等功能;
(2)、在分布式系统下不能使用暂停/恢复/删除等功能;
(3)、由于quartz线程池大小有限,如果是任务量比较大,会排队等待较长时间;

四、springboot集成xxl-job

分布式xxl-job平台是一个独立的服务平台。一般需要配套Mysql、自身admin的jar包部署到Tomact上。那么接下来先介绍admin部署过程。

4.1、编译启动xxl-job-admin
导入IDEA;
执行sql脚本【配套mysql】;
修改修改application.properties配置文件参数【主要是spring.datasource进行配置】;
启动XxlJobAdminApplication,默认地址http://127.0.0.1:8080/xxl-job-admin,账号密码admin/123456;

4.2、springboot集成xxl-job
引用jar包

<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.0</version>
</dependency>

Application.properties配置

#xxl-job
### 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://localhost:8081/xxl-job-admin
### 执行器通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
xxl.job.executor.address=
### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
xxl.job.executor.ip=
### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
xxl.job.executor.port=9999
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
xxl.job.executor.logretentiondays=30

Config配置

@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.executor.appname}")
private String appName;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appName);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}

业务代码

@Component
public class XXLScheduledTestV1 extends IJobHandler {

@XxlJob("scheduledTest1")
public ReturnT<String> scheduledTest1(){
//处理业务
System.out.println(Thread.currentThread()+" scheduledTest1任务, time:"+new Date());
return ReturnT.SUCCESS;
}
@Override
public void execute() throws Exception {
System.out.println(Thread.currentThread()+" XXLScheduledTestV1, execute() :"+new Date());
}
}

接下来在admin平台上进行配置

然后执行就可以了。

五、任务调度技术的选择

选择@Scheduled注解

存在问题:
(1)、单线程执行,如果某个任务阻塞会影响其他任务;
-----多线程解决。但是,如何设置coreSize需要调整,否则还是有任务阻塞等待。
(2)、某个任务失败,后续还会继续执行,并且任务随着系统服务启动而开启,不能终止或中途添加;
(3)、只能适用于单机服务架构,不适合分布式场景;

选择ScheduledThreadPoolExecutor
对于随机时刻触发场景【如订单流转】,那么需要考虑ScheduledThreadPoolExecutor来解决。
存在问题:
ScheduledThreadPoolExecutor功能能满足大部分场景,但是也存在明显的不足:(1)、任务有异常,不能单独暂停该任务;(2)、调度cron时间配置不灵活;(3)、不能灵活开启关闭任务。

选择quartz
类似的ScheduledThreadPoolExecutor 的scheduleAtFixedRate方法,但是可以随时开启暂停任务、调整调度时间频率。
一般简单的功能,不需要太多的配置,采用默认配置即可。一些要求较高的使用场景,需要对quartz.properties进行参数配置【如利用quartz数据库持久化功能】。

存在问题:
(1)、适合单机服务,可以使用任务启动/暂停/恢复/删除等功能;
(2)、在分布式系统下不能使用暂停/恢复/删除等功能;
(3)、由于quartz线程池大小有限,如果是任务量比较大,会排队等待较长时间;

选择分布式xxl-job
如果考虑要求高、分布式调度等场景,那么需要考虑分布式调度框架如xxl-job。

posted @ 2022-09-17 11:19  liftsail  阅读(1703)  评论(0编辑  收藏  举报