在实际项目开发中,定时任务调度是不可缺少的,在Spring Boot中提供了 @Scheduled
注解实现了定时调度的功能,其用法也是非常的简单如下:
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(QuartzApplication.class, args);
}
/**
* initialDelay(和initialDelayString),第一次调度之前需要延迟多久
* fixedRate(和fixedRateString),每隔多久时间需要调度一次
* fixedDelay(和fixedDelayString),两次调度之间需要加一个固定的延迟时间
* zone,解析cron表达式的时候解析时区
*/
//第一次执行之前延后10秒钟;后续每隔5秒执行1次
@Scheduled(fixedRate = 5000, initialDelay = 10000)
public void task1() {
SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
System.out.println("当前时间:" + dateFormat.format(new Date()));
}
//使用cron表达式,每5秒执行一次
@Scheduled(cron = "0/5 * * * * ?")
public void task2() {
SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
System.out.println("当前时间:" + dateFormat.format(new Date()));
}
}
SpringBoot中设置调度任务非常的简单,其实其实现并不难,我们也可以自己来实现,如 Timer 、ScheduledExecutor 、 Quartz ,这里我们重点介绍了功能强大的Quartz,其实现简单,并且支持高可用,可多节点部署,通过竞争数据库锁来保证只有一个节点执行任务。
有关具体的介绍,可查看 定时任务框架Quartz
这里我们就来简单的看一看在SpringBoot中如何使用Quartz,首先需要引入相关依赖,如下:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-quartzartifactId>
dependency>
然后我们先来定义一个处理业务逻辑的Job,如下:
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
String value = dataMap.get("name").toString();
System.out.println(value);
}
}
再在项目中使用 @Bean 的方式将其注入即可,还是定时任务框架Quartz中介绍的相关内容,如下:
@SpringBootApplication
@EnableScheduling
public class QuartzApplication {
public static void main(String[] args) {
SpringApplication.run(QuartzApplication.class, args);
}
@Bean("jobDetail")
public JobDetailFactoryBean jobDetailFactoryBean() {
JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
jobDetailFactoryBean.setJobClass(MyJob.class);
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("name", "MyQuartz");
jobDetailFactoryBean.setJobDataAsMap(jobDataMap);
return jobDetailFactoryBean;
}
@Bean("cronJobTrigger")
public CronTriggerFactoryBean cronTriggerFactoryBean(JobDetailFactoryBean jobDetail) {
CronTriggerFactoryBean triggerFactoryBean = new CronTriggerFactoryBean();
triggerFactoryBean.setJobDetail(jobDetail.getObject());
triggerFactoryBean.setCronExpression("0/2 * * * * ?");
return triggerFactoryBean;
}
/**
* 调度工厂,默认是StdSchedulerFactory
* 通过getScheduler()得到调度器----StdScheduler
* Scheduler中有一个QuartzSchedulerThread线程实例---即调度线程
* 调度线程---不停地获取触发器---当触发器触发时,从线程池中取线程执行对应的job
*/
@Bean("scheduler")
public SchedulerFactoryBean schedulerFactoryBean(Trigger cronJobTrigger) {
SchedulerFactoryBean bean = new SchedulerFactoryBean();
bean.setOverwriteExistingJobs(true);
bean.setStartupDelay(1);
bean.setTriggers(cronJobTrigger);
return bean;
}
}
随着系统规模的发展,系统可能从单机部署到集群部署,在集群中调度任务对集群机器中配置的任务,只能运行一个(排它处理),另外随着业务越来越多,相应的定时任务也会增多,单台服务器执行任务的压力会越来越大
这里我们想到的最简单的方式就是加锁或者进行选举。 如上述介绍的Quartz在分布式环境下,就可以通过竞争数据库锁来保证只有一个节点执行任务。但是也存在缺点,比如性能差,无法适应高并发场景;容易出现死锁的情况;无法优雅的实现阻塞式锁。
不过这里我们就无需自己来处理,分布式下任务调度有很多开源的解决方案,如Elastic-Job 、xxl-job 等等,这里我们就先来了解下Elastic-Job框架。
Elastic job是当当网开源提供的开源分布式调度工具,其基于Zookeeper、Quartz开发并开源的一个Java分布式定时任务,解决了Quartz不支持分布式的弊端。Elastic job主要的功能有支持弹性扩容,通过Zookepper集中管理和监控job,支持失效转移等。项目由两个相互独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成,其最大的区别是有无调度中心。
Elastic job会将直接把任务分摊出去,每台机器负责哪些任务都会分配好,使用Elastic job首先我们需要引入相关依赖,如下:
<dependency>
<groupId>com.dangdanggroupId>
<artifactId>elastic-job-lite-coreartifactId>
<version>2.1.5version>
dependency>
<dependency>
<groupId>com.dangdanggroupId>
<artifactId>elastic-job-lite-springartifactId>
<version>2.1.5version>
dependency>
然后就可以来实现业务处理的逻辑,Job可以有好几种作业类型,如果只是简单的任务,实现S impleJob 即可,并且这里我们会做任务分片处理,如下:
public class MyJob implements SimpleJob {
@Override
public void execute(ShardingContext shardingContext) {
System.out.println("当前分片:" + shardingContext.getShardingParameter());
int shardingItem = shardingContext.getShardingItem();
int total = shardingContext.getShardingTotalCount();
if (shardingItem == 1) {
System.out.println(String.format("ThreadId:%s, 当前分片项:%s", Thread.currentThread().getId(), shardingItem));
} else {
System.out.println(String.format("ThreadId:%s, 当前分片项:%s", Thread.currentThread().getId(), shardingItem));
}
}
}
任务分片解释:某些量大的任务,能够通过分拆分摊,提高执行时效,如:给1万用户发短信或邮件等,此时,可做为分片任务来处理,即任务分拆成多片小任务。任务触发时,每片任务发送到一台机器去处理,如1万用户发短信,拆成两个任务(每个发5000条短信),分别到达两台机器去执行。
然后我们还需要对Elastic-Job进行相关的配置,如下:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:reg="http://www.dangdang.com/schema/ddframe/reg"
xmlns:job="http://www.dangdang.com/schema/ddframe/job"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.dangdang.com/schema/ddframe/reg
http://www.dangdang.com/schema/ddframe/reg/reg.xsd
http://www.dangdang.com/schema/ddframe/job
http://www.dangdang.com/schema/ddframe/job/job.xsd">
<reg:zookeeper id="jobRegisterCenter"
server-lists="127.0.0.1:2181"
namespace="elastic-job"
base-sleep-time-milliseconds="1000"
max-sleep-time-milliseconds="3000"
max-retries="3"/>
<job:simple id="testJob" class="com.kami.MyJob"
registry-center-ref="jobRegisterCenter"
cron="0/5 * * * * ?"
disabled="false"
overwrite="true"
sharding-total-count="2"
sharding-item-parameters="0=A,1=B"
description="测试Elastic Job"/>
beans>
这里我们如果启动一个项目,那么分片就暂不会发挥作用,这里我们启动两个任务,启动类如下:
@SpringBootApplication
@ImportResource({"classpath:elastic-job.xml"})
public class ElasticJobApplicationA {
public static void main(String[] args) {
SpringApplication.run(ElasticJobApplicationA.class, args);
}
}
@SpringBootApplication
@ImportResource({"classpath:elastic-job.xml"})
public class ElasticJobApplicationB {
public static void main(String[] args) {
SpringApplication.run(ElasticJobApplicationB.class, args);
}
}
这里我们启动两个项目时,需要更改 application.yml 中的端口配置server.port,并且在启动一个启动类时,需要将另一个启动类上注解暂时注释掉,否则会报异常,测试结果如下:
这里如果我们将其中一个项目停止,那么所有的任务就会有同一个项目进行处理(需等待一段时间),如下:
上述是使我们使用的xml方式来配置的,如果想要更加简洁的配置,我们可以通过一个GitHub上的项目来实现,GitHub地址:https://github.com/yinjihuan/elastic-job-spring-boot-starter,这个需要我们添加仓库地址之后,才可以拉取到相关的依赖。
所以首先我们需要在maven的安装路径下的 conf 文件下的 settings.xml 文件中,添加如下仓库地址
<repositories>
<repository>
<id>jitpack.ioid>
<url>https://jitpack.iourl>
repository>
repositories>
然后就可以来取到相关的依赖了,如下:
<dependency>
<groupId>com.cxytiandigroupId>
<artifactId>elastic-job-spring-boot-starterartifactId>
<version>1.0.4version>
dependency>
然后我们就无需配置xml文件了,直接在 yml 中配置即可,如下:
server:
port: 8080
elastic:
job:
zk:
serverLists: 127.0.0.1:2181
namespace: elastic-job
myJob:
cron: 0/5 * * * * ?
shardingTotalCount: 2
shardingItemParameters: 0=A,1=B
这里我们在启动类中就无需引入xml文件了,不过这里我们需要加上 @EnableElasticJob
注解,如下:
@SpringBootApplication
@EnableElasticJob
//@ImportResource({"classpath:elastic-job.xml"})
public class ElasticJobApplication {
public static void main(String[] args) {
SpringApplication.run(ElasticJobApplication.class, args);
}
}