定时任务的实现方式
1.java.utils.Timer
创建任务的常用方法如下:
// 在指定延迟后,以固定延迟执行任务
void schedule(TimerTask task, long delay, long period);
// 在指定延迟后,以固定速率执行任务
void scheduleAtFixedRate(TimerTask task, long delay, long period);
TimerTask
是一个实现了Runnable接口的抽象类,具体的任务执行逻辑需要继承TimerTask
并重写run方法。- delay参数是任务第一次执行时延迟的时间。还有几个重载的方法没有列出,将delay修改为Date类型可以直接指定第一次运行的时间。
- period表示两次任务执行的时间间隔,名称为schedule的几个方法,如果去掉period参数表示任务只执行一次。
使用示例:
schedule
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
Thread.sleep(500L);
System.out.println(new Date() + "执行任务");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, 0, 1000);
}
scheduleAtFixedRate使用和schedule一样。
不同点在于schedule按照固定延迟执行任务,更注重保持两次任务执行的时间间隔。scheduleAtFixedRate按照固定速率执行任务,更注重保持执行频率的稳定。如果因为某些问题导致任务执行超时,错过了下次任务的执行时间,schedule会忽略错过的任务,而scheduleAtFixedRate努力追上原来的节奏确保相同时间段内的执行次数一致。
2.ScheduledExecutorService
是JDK中的定时任务接口,基于ThreadPoolExecutor
,除了拥有线程池的功能外,添加了下面几个创建定时任务的接口:
ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
<V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit);
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit);
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit);
参数和Timer相似,Runnable、Callable参数会被ScheduledExecutorService包装成ScheduledFutureTask让线程池中的线程执行。
使用示例:
static Runnable taskRunnable;
static {
taskRunnable = () -> {
try {
Thread.sleep(500L);
System.out.println(new Date() + "执行任务");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
};
}
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
// 固定延迟
executor.scheduleWithFixedDelay(taskRunnable, 0, 1000, TimeUnit.MILLISECONDS);
// 固定速率
executor.scheduleAtFixedRate(taskRunnable, 0, 1000, TimeUnit.MILLISECONDS);
}
3.Spring Task
简介
Spring3.0自带的定时任务,实际使用的就是ScheduledExecutorService
而且Spring默认使用Executors.newSingleThreadScheduledExecutor()
方法创建只有一个线程的线程池。如果要修改线程池数量,需要实现SchedulingConfigurer
的configureTasks
方法:
@Configuration
public class SpringSchedulingConfigurer implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
}
}
在Springboot项目中使用Spring Task时,需要在启动类上添加@EnableScheduling
注解,然后在Sping Bean中使用@Scheduled
定义定时任务。
定义固定延迟的定时任务:
@Component
public class SpringTaskImpl {
// fixedDelay=2000,下次任务执行时间 = 本次任务执行结束时间 + 2秒
@Scheduled(fixedDelay = 2000L)
public void job1() {
log.info("fixedDelay定时任务");
}
}
定义固定速率的定时任务:
@Component
public class SpringTaskImpl {
// fixedRate=2000,下次任务执行时间 = 本次任务执行开始时间 + 2秒
@Scheduled(fixedRate = 2000L)
public void job2() {
log.info("fixedRate定时任务");
}
}
相比之前的方式,Spring Task多了一种使用cron表达式来指定任务在某个时间点或周期性执行的方式。
使用cron表达式定义定时任务:
@Component
public class SpringTaskImpl {
@Scheduled(cron = "*/2 * * * * ?")
public void job3() {
log.info("cron定时任务");
}
}
cron表达式介绍
一个cron表达式有至少6个(也可能7个)有空格分隔的时间元素。
* * * * * * *
┬ ┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ │ │
│ │ │ │ │ │ └─年(可选,1970+)
│ │ │ │ │ └── 星期(1 - 7或者MON - SUN,1表示星期日)
│ │ │ │ └───── 月份(1 - 12)
│ │ │ └────────── 日(1 - 31)
│ │ └─────────────── 小时(0 - 23)
│ └──────────────────── 分钟(0 - 59)
└───────────────────────── 秒(0 - 59)
其中每个元素可以是一个值(如6)、一个连续区间(9-12)、一个间隔时间(8-18/4)(/表示每隔4小时)、一个列表(1,3,5)、通配符。
星号(*):表示匹配任意值。例如,* 在分钟字段中表示每分钟都执行。
逗号(,):用于分隔多个值。例如,1,3,5 在小时字段中表示 1 点、3 点和 5 点执行。
斜线(/):用于指定间隔值。例如,*/5 在分钟字段中表示每 5 分钟执行一次。
连字符(-):用于指定范围。例如,10-20 在日期字段中表示从 10 号到 20 号。
问号(?):仅用于日期和星期几字段,表示不指定具体值。在表达式中必须且只能出现一次。
井号(#):主要用于指定在一个月中的第几个星期几
cron表达式例子:
0 0 10,14,16 * * ? 每天10:00,14:00,16:00
0 0/30 9-17 * * ? 每天9点到17点每30分钟
0 0 12 ? * WED 每个星期三12:00
0 15 10 * * ? 2024 2024年的每天10:15
0 15 10 ? * 6#3 每月第三个星期五10:15
4.分布式定时任务
上面的几种定时任务实现方案都是针对单体式的程序,不支持集群配置。如果部署到多个节点,各节点之间没有任何协调通讯机制,都按时执行任务会导致任务重复执行。
如果需要在分布式环境下使用定时任务,可以使用quartz、xxl-job、elastic-job等。
quartz
quartz 是一个开源的作业调度框架,它提供了强大的作业调度功能,包括定时任务、复杂的分布式任务,以及对任务动态管理的能力,如启动、暂停、恢复、停止和修改触发时间等。
在quartz中,一个定时任务主要由三个部分组成:
- Job:需要定时执行的任务
- Trigger:触发器,指定运行参数
- Scheduler:调度器,将Job和Trigger组装起来,使定时任务被真正执行
使用示例:
public static void main(String[] args) throws SchedulerException {
// 1、创建调度器Scheduler
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
// 2、创建JobDetail
JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class)
.withIdentity("job", "group")
.build();
// 3、构建Trigger实例,每隔1s执行一次
Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger", "triggerGroup")
.startNow()// 立即生效
// .withSchedule(SimpleScheduleBuilder.simpleSchedule()
// .withIntervalInSeconds(2)// 每隔2s执行一次
// .repeatForever())
.withSchedule(CronScheduleBuilder.cronSchedule("*/2 * * * * ?"))
.build();// 一直执行
// 4、Scheduler绑定Job和Trigger,并执行
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
}
public static class QuartzJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) {
try {
Thread.sleep(500L);
System.out.println(new Date() + "执行任务");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
quartz对分布式的支持不友好,通常都需要进行改造才能实现功能需求。
xxl-job2.1.0之前的版本和elastic-job都是基于quartz进行任务调度的。
XXL-JOB
1.系统组成
是一个分布式任务调度平台,将定时任务的调度和任务执行分离。
调度模块中不包含业务代码,只需要根据调度配置(cron表达式、固定速度)在到达某个时间点或一个周期后,向任务执行模块发送调度请求。
执行模块只需要在接收到调度请求后,进行任务逻辑执行、终止、日志加载操作。
架构图:
- 执行器通过注册线程,将自己的ip、端口、appname等信息注册到调度中心
- 达到任务触发条件时,调度器向执行器下发任务
- 执行器执行任务,将执行结果放在内存队列中,将执行日志保存到文件
- 执行器中用一个线程消费执行结果的队列,将结果主动发送给调度中心
- 调度中心查看日志时请求执行器,由执行器返回日志内容
2.调度中心
在源码中调度中心在xxl-job-admin
模块,除了触发调度执行外,还提供Web页面对任务进行管理。
调度中心依赖数据库,xxl-job使用的是mysql。在我们的项目中需要将源码里的数据库初始化SQL脚本修改成pgsql的版本,xxl-job-admin模块的代码也需要做一点点修改。
启动调度中心
- 修改配置内容
### 调度中心JDBC链接:链接地址请保持和 2.1章节 所创建的调度数据库的地址一致
spring.datasource.url=jdbc:postgresql://192.168.1.231:5432/public?currentSchema=xxl_job&stringtype=unspecified
spring.datasource.username=publica
spring.datasource.password=123456
spring.datasource.driver-class-name=org.postgresql.Driver
### 报警邮箱
spring.mail.host=
spring.mail.port=25
spring.mail.username=xxx@
spring.mail.password=xxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
### 调度中心通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=
### 调度中心国际化配置 [必填]: 默认为 "zh_CN"/中文简体, 可选范围为 "zh_CN"/中文简体, "zh_TC"/中文繁体 and "en"/英文;
xxl.job.i18n=zh_CN
## 调度线程池最大线程配置【必填】
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100
### 调度中心日志表数据保存天数 [必填]:过期日志自动清理;限制大于等于7时生效,否则, 如-1,关闭自动清理功能;
xxl.job.logretentiondays=30
- 编译打包部署
成功启动后,通过http://localhost:8080/xxl-job-admin访问调度中心。
默认登录账号 “admin/123456”
3.执行器
配置部署执行器
- 添加依赖
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.4.0</version>
</dependency>
- 配置执行器
创建一个执行器组件XxlJobSpringExecutor
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
// 调度中心的地址
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
// appname,是执行器集群的唯一标示
xxlJobSpringExecutor.setAppname(appName);
// 注册到调度中心上的地址,为空时使用当前应用的IP:PORT。手动设置可以更灵活的支持容器类型执行器动态IP和动态映射端口问题
xxlJobSpringExecutor.setAddress(address);
// 默认为空表示自动获取IP,多网卡时可手动设置指定IP
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
// 日志保存路径和保留天数
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
- 开发Job方法
只需要在执行任务的方法上添加一个注解@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")
,最重要的是value的值,在调度中心新建任务时填写的jobhandler。
@Component
public class SimpleTask {
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {
// 通过 "XxlJobHelper.log" 打印执行日志
XxlJobHelper.log("XXL-JOB, Hello World.");
// 设置任务结果为成功(默认成功,可以省略)
XxlJobHelper.handleSuccess();
}
}
- 部署
启动执行器所在的程序,然后去调度中心的Web页面上查看。
在执行器管理页面没有看到刚刚注册的执行器,新的执行器必须先手动创建。
在调度中心创建一个执行器,appname和执行器程序中的配置相同,然后等执行器程序下一次注册(30s一次)后才能在页面上看到变化。
4.任务管理
创建一个任务
创建成功后任务的状态是stop,需要启动调度中心才会自动调度。
点击执行一次
按钮,可以手动触发立即执行一次任务。
点击查询日志
按钮,查看任务调度的历史、日志和调度结果,查看任务执行日志。
任务配置属性详细说明
基础配置:
- 执行器:任务的绑定的执行器,任务触发调度时将会自动发现注册成功的执行器, 实现任务自动发现功能; 另一方面也可以方便的进行任务分组。每个任务必须绑定一个执行器, 可在 "执行器管理" 进行设置;
- 任务描述:任务的描述信息,便于任务管理;
- 负责人:任务的负责人;
- 报警邮件:任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔;
调度配置:
- 调度类型:
无:该类型不会主动触发调度;
CRON:该类型将会通过CRON,触发任务调度;
固定速度:该类型将会以固定速度,触发任务调度;按照固定的间隔时间,周期性触发;
- CRON:触发任务执行的Cron表达式;
- 固定速度:固定速度的时间间隔,单位为秒;
任务配置:
创建GLUE模式的任务,以powershell为例。GLUE模式的任务能使用Web IDE编辑;有版本回溯功能;编辑完保存后实时生效;
- 运行模式:
BEAN模式:任务以JobHandler方式维护在执行器端;需要结合 "JobHandler" 属性匹配执行器中任务;
GLUE模式(Java):任务以源码方式维护在调度中心;该模式的任务实际上是一段继承自IJobHandler的Java类代码并 "groovy" 源码方式维护,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务;
GLUE模式(Shell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "shell" 脚本;
GLUE模式(Python):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "python" 脚本;
GLUE模式(PHP):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "php" 脚本;
GLUE模式(NodeJS):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "nodejs" 脚本;
GLUE模式(PowerShell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "PowerShell" 脚本;
- JobHandler:运行模式为 "BEAN模式" 时生效,对应执行器中新开发的JobHandler类“@JobHandler”注解自定义的value值;
- 执行参数:任务执行所需的参数;
高级配置:
启动多个执行器,使用不同的路由策略下发任务。
- 路由策略:当执行器集群部署时,提供丰富的路由策略,包括;
FIRST(第一个):固定选择第一个机器;
LAST(最后一个):固定选择最后一个机器;
ROUND(轮询):;
RANDOM(随机):随机选择在线的机器;
CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。
LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;
LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举;
FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;
BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;
- 子任务:每个任务都拥有一个唯一的任务ID(任务ID可以从任务列表获取),当本任务执行结束并且执行成功时,将会触发子任务ID所对应的任务的一次主动调度。
- 调度过期策略:
- 忽略:调度过期后,忽略过期的任务,从当前时间开始重新计算下次触发时间;
- 立即执行一次:调度过期后,立即执行一次,并从当前时间开始重新计算下次触发时间;
- 阻塞处理策略:调度过于密集执行器来不及处理时的处理策略;
单机串行(默认):调度请求进入单机执行器后,调度请求进入FIFO队列并以串行方式运行;
丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败;
覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务;
- 任务超时时间:支持自定义任务超时时间,任务运行超时将会主动中断任务;
- 失败重试次数;支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;
5.集群部署
调度中心集群部署有几点要求和建议:
- 数据库配置保持一致,调度中心通过mysql(我们的项目里是pgsql)悲观锁实现分布式锁,必须连接到同一数据库
- 集群机器时钟保持一致(单机集群忽视)
- 建议:推荐通过nginx为调度中心集群做负载均衡,分配域名。调度中心访问、执行器回调配置、调用API服务等操作均通过该域名进行
6.常用API
登录:/login?userName=%s&password=%s
添加任务:/jobinfo/add
修改任务:/jobinfo/update
删除任务:/jobinfo/remove?id=%s
开启任务:/jobinfo/start?id=%s
停止任务:/jobinfo/stop?id=%s
立即执行任务:/jobinfo/trigger
执行器列表:/jobgroup/pageList
任务列表:/jobinfo/pageList