定时任务
定时任务在实际的开发中特别常见,比如电商平台 30 分钟后自动取消未支付的订单,以及凌晨的数据汇总和备份等,都需要借助定时任务来实现,本文记录了定时任务常见的几种实现方式。
转载文章:定时任务最简单的3种实现方法(Java)
参考文章:Java实现定时任务、Quartz 定时任务(Scheduler)的 3 种实现方式
推荐文章:任务调度框架Quartz(三)任务调度框架Quartz实例详解深入理解Scheduler,Job,Trigger,JobDetail
Java 语言实现定时任务
在 Java 语言中,实现定时任务有几种常用的方法:
java.util.Timer类:这是Java标准库提供的一个类,可以用来安排任务以后在后台线程中执行。使用 Timer 类,你可以创建一个 TimerTask 任务,然后使用 schedule 或 scheduleAtFixedRate 方法来安排任务的执行。ScheduledExecutorService接口:这是Java并发包中的一部分,提供了更灵活的定时任务调度能力。你可以使用 Executors 类创建一个 ScheduledExecutorService 实例,然后使用 schedule 或 scheduleAtFixedRate 方法来安排任务。- Spring框架的 
@Scheduled注解:如果你在使用Spring框架,可以利用 @Scheduled 注解来简化定时任务的配置。Spring的调度器会根据注解的参数来执行相应的方法。 Quartz Scheduler:这是一个开源的作业调度库,提供了比Java标准库更强大的定时任务功能。Quartz允许你配置复杂的调度策略,如cron表达式,并支持集群。
Timer
Timer 是 JDK 自带的定时任务执行类,无论任何项目都可以直接使用 Timer 来实现定时任务,所以 Timer 的优点就是使用方便.
定时任务的实现代码
1  | 
  | 
1  | Run timerTask:Mon Aug 17 21:29:25 CST 2020  | 
Timer 缺点分析
任务执行时间长影响其他任务
当一个任务的执行时间过长时,会影响其他任务的调度,如下代码所示:
1  | 
  | 
1  | 进入 timerTask 1:Mon Aug 17 21:44:08 CST 2020  | 
当任务 1 运行时间超过设定的间隔时间时,任务 2 也会延迟执行。原本任务 1 和任务 2 的执行时间间隔都是 3s,但因为任务 1 执行了 5s,因此任务 2 的执行时间间隔也变成了 10s(和原定时间不符)。
任务异常影响其他任务
使用 Timer 类实现定时任务时,当一个任务抛出异常,其他任务也会终止运行,如下代码所示:
1  | 
  | 
1  | 进入 timerTask 1:Mon Aug 17 22:02:37 CST 2020  | 
小结
Timer 类实现定时任务的优点是方便,因为它是 JDK 自定的定时任务,但缺点是任务如果执行时间太长或者是任务执行异常,会影响其他任务调度,所以在生产环境下建议谨慎使用。
ScheduledExecutorService
ScheduledExecutorService 也是 JDK 1.5 自带的 API,我们可以使用它来实现定时任务的功能,也就是说 ScheduledExecutorService 可以实现 Timer 类具备的所有功能,并且它可以解决了 Timer 类存在的所有问题。
实现定时任务的代码
1  | 
  | 
1  | Run Schedule:Mon Aug 17 21:44:23 CST 2020  | 
可靠性测试
1、任务超时执行测试
首先,测试当一个任务执行时间过长,会不会对其他任务造成影响,测试代码如下:
1  | 
  | 
1  | Run Schedule2:Mon Aug 17 11:27:55 CST 2020  | 
测试结果:当任务 1 执行时间 5s 超过了执行频率 3s 时,并没有影响任务 2 的正常执行。 所以 ScheduledExecutorService 可以解决 Timer 任务之间相应影响的缺点
2、任务异常测试
其次,测试在一个任务异常时,是否会对其他任务造成影响,测试代码如下:
1  | 
  | 
1  | 进入 Schedule:Mon Aug 17 22:17:37 CST 2020  | 
测试结果:当任务 1 出现异常时,并不会影响任务 2 的执行。
小结
在单机生产环境下建议使用 ScheduledExecutorService 来执行定时任务,它是 JDK 1.5 之后自带的 API,因此使用起来也比较方便,并且使用 ScheduledExecutorService 来执行任务,不会造成任务间的相互影响。
Spring Task
如果使用的是 Spring 或 Spring Boot 框架,可以直接使用 Spring Framework 自带的定时任务,使用上面两种定时任务的实现方式,很难实现设定了具体时间的定时任务,比如当我们需要每周五来执行某项任务时,但如果使用 Spring Task 就可轻松的实现此需求。 以 Spring Boot 为例,实现定时任务只需两步:
开启定时任务
开启定时任务只需要在 Spring Boot 的启动类上声明 @EnableScheduling 即可,实现代码如下:
1  | 
  | 
添加定时任务
添加定时任务 定时任务的添加只需要使用 @Scheduled 注解标注即可,如果有多个定时任务可以创建多个 @Scheduled 注解标注的方法,示例代码如下:
1  | 
  | 
注意:定时任务是自动触发的无需手动干预,也就是说 Spring Boot 启动后会自动加载并执行定时任务。
Cron表达式
Spring Task 的实现需要使用 cron 表达式来声明执行的频率和规则,cron 表达式是由 6 位或者 7 位组成的(最后一位可以省略),每位之间以空格分隔,表达式语法如下:
1  | [秒] [分] [小时] [日] [月] [周] [年]  | 
每位从左到右代表的含义如下表所示,其中* 和 ?号都表示匹配所有的时间。
| 字段 | 允许值 | 允许的特殊字符 | 
|---|---|---|
| 秒(Seconds) | 0~59的整数 | , - * / 四个字符 | 
| 分(Minutes) | 0~59的整数 | , - * / 四个字符 | 
| 小时(Hours) | 0~23的整数 | , - * / 四个字符 | 
| 日期(DayofMonth) | 1~31的整数(但是你需要考虑你月的天数) | , - * ? / L W C 八个字符 | 
| 月份(Month) | 1~12的整数或者 JAN-DEC | , - * / 四个字符 | 
| 星期(Dayofweek) | 1~7的整数或者 SUN-SAT (1=SUN) | , - * ? / L C # 八个字符 | 
| 年(可选,留空)(Year) | 1970~2099 | , - * / 四个字符 | 
cron 表达式在线生成地址:https://cron.qqe2.com/
通配符说明
*表示所有值。 例如:在分的字段上设置 *,表示每一分钟都会触发。?表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为”?” 具体设置为 0 0 0 10 * ?-表示区间。例如 在小时上设置 “10-12”,表示 10,11,12点都会触发。,表示指定多个值,例如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发/用于递增触发。如在秒上面设置”5/15” 表示从5秒开始,每增15秒触发(5,20,35,50)。 在月字段上设置’1/3’所示每月1号开始,每隔三天触发一次。L表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于”7”或”SAT”。如果在”L”前加上数字,则表示该数据的最后一个。例如在周字段上设置”6L”这样的格式,则表示“本月最后一个星期五”W表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上置”15W”,表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 “1W”,它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,”W”前只能设置具体的数字,不允许区间”-“)。#序号(表示每月的第几个周几),例如在周字段上设置”6#3”表示在每月的第三个周六.注意如果指定”#5”,正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了) ;小提示:’L’和 ‘W’可以一组合使用。如果在日字段上设置”LW”,则表示在本月的最后一个工作日触发;周字段的设置,若使用英文字母是不区分大小写的,即MON与mon相同。
常用Cron表达式
1  | 0/2 * * * * ? 表示每2秒 执行任务  | 
cron表达式支持占位符
配置文件:
1  | time:  | 
主程序:
1  | 
  | 
zone:时区,接收一个
java.util.TimeZone。cron表达式会基于该时区解析。默认是一个空字符串,即取服务器所在地的时区。比如我们一般使用的时区Asia/Shanghai。该字段一般留空。fixedDelay:上一次执行完毕时间点之后多长时间再执行。
1
// 上一次执行完毕时间点之后5秒再执行
fixedDelayString:与
fixedDelay意思相同,只是使用字符串的形式。唯一不同的是支持占位符。1
fixedRate:上一次开始执行时间点之后多长时间再执行。
1
// 上一次开始执行时间点之后5秒再执行
fixedRateString:与
fixedRate意思相同,只是使用字符串的形式。唯一不同的是支持占位符。initialDelay: 第一次延迟多长时间后再执行。
1
// 第一次延迟1秒后执行,之后按fixedRate的规则每5秒执行一次
initialDelayString: 与
initiaDelay意思相同,只是使用字符串的形式。唯一不同的是支持占位符。
Quartz Scheduler
Quartz Scheduler 是一个开源的作业调度库,提供了比Java标准库更强大的定时任务功能。Quartz允许配置复杂的调度策略,如cron表达式,并支持集群。Spring boot 官网文档:Quartz Scheduler ,推荐文章:Quartz-scheduler 定时器概述、核心 API 与 快速入门
引入依赖
1  | <!-- quartz 定时任务调度 -->  | 
实现方式一
定义好定时任务的业务内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Date;
public class Show implements Job {
public void execute(JobExecutionContext arg0) throws JobExecutionException {
log.info("\n\n-------------------------------\n It is running and the time is : {}" +
"\n-------------------------------\n", new Date());
}
}声明定时任务,并关联业务实现类 。在
JobDetail jb = JobBuilder.newJob(Show.class)中关联业务类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Date;
public class SchedulerTest {
public static void main(String[] args) {
try {
// 1.创建Scheduler的工厂
SchedulerFactory sf = new StdSchedulerFactory();
// 2.从工厂中获取调度器实例
Scheduler scheduler = sf.getScheduler();
// 3.创建JobDetail
JobDetail jb = JobBuilder.newJob(Show.class) // Show 为一个job,是要执行的一个任务。
.withDescription("这是我的测试定时任务。") // job的描述
.withIdentity("jy2Job", "jy2Group") // job 的name和group
.build();
// 任务运行的时间,SimpleSchedle类型触发器有效
long time = System.currentTimeMillis() + 3 * 1000L; // 3秒后启动任务
Date statTime = new Date(time);
// 4.创建Trigger
// 使用SimpleScheduleBuilder或者CronScheduleBuilder
Trigger t = TriggerBuilder.newTrigger()
.withDescription("")
.withIdentity("jyTrigger", "jyTriggerGroup")
//.withSchedule(SimpleScheduleBuilder.simpleSchedule())
.startAt(statTime) // 默认当前时间启动 ,也可以写为:.startNow();
.withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?")) // 两秒执行一次
.build();
// 5.注册任务和定时器
scheduler.scheduleJob(jb, t);
// 6.启动 调度器
scheduler.start();
log.info("启动时间 :{}" , new Date());
} catch (Exception e) {
log.info("定时任务出现异常 : {}", e);
}
}
}
实现方式二
定义好定时任务的业务内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Date;
public class Show implements Job {
public void execute(JobExecutionContext arg0) throws JobExecutionException {
log.info("\n\n-------------------------------\n It is running and the time is : {}" +
"\n-------------------------------\n", new Date());
}
}定义好定时任务的触发类,调用业务类中的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import org.quartz.JobExecutionException;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Date;
import lombok.extern.slf4j.Slf4j;
public class UserSyncTask {
Show show;
public void cronDepartmentsAndUsersJob() {
log.info("\n\n 定时--开始,当前时间: {}", dateFormat().format(new Date()));
try {
show.execute(null);
} catch (JobExecutionException e) {
e.printStackTrace();
}
log.info("\n\n 定时--结束,当前时间:{}", dateFormat().format(new Date()));
}
private SimpleDateFormat dateFormat() {
return new SimpleDateFormat("HH:mm:ss");
}
}配置文件中:配置触发类和任务执行频率
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
<!--定时任务触发类-->
<bean id="userSyncTask" class="gentle.test.UserSyncTask"></bean>
<!--执行频率-->
<task:scheduled-tasks>
<!--每 2 秒执行一次-->
<task:scheduled ref="userSyncTask" method="cronDepartmentsAndUsersJob" cron="0/2 * * * * ?" />
</task:scheduled-tasks>
</beans>
实现方式三
引入 jar , 同上
运行类,代码中给 2 个注解:
   @EnableScheduling  // 开启定时器
   @Scheduled(fixedDelay = 2000)  或者 @Scheduled(cron = "* * 2 * * ?")   // 每 2s 执行 1 次 。
- 代码:
 
1  | import gentle.util.DateUtil;  | 
拓展:分布式定时任务
以上都是关于单机定时任务的实现,如果是分布式环境可以使用 Redis 来实现定时任务。 使用 Redis 实现延迟任务的方法大体可分为两类:通过 ZSet 的方式和键空间通知的方式。
ZSet 实现方式
通过 ZSet 实现定时任务的思路是,将定时任务存放到 ZSet 集合中,并且将过期时间存储到 ZSet 的 Score 字段中,然后通过一个无线循环来判断当前时间内是否有需要执行的定时任务,如果有则进行执行,具体实现代码如下:
1  | import redis.clients.jedis.Jedis;  | 
键空间通知
我们可以通过 Redis 的键空间通知来实现定时任务,它的实现思路是给所有的定时任务设置一个过期时间,等到了过期之后,我们通过订阅过期消息就能感知到定时任务需要被执行了,此时我们执行定时任务即可。 默认情况下 Redis 是不开启键空间通知的,需要我们通过  config set notify-keyspace-events Ex  的命令手动开启,开启之后定时任务的代码如下:
1  | import redis.clients.jedis.Jedis;  | 
定时任务部署 xxl-job
参考文章链接
参考文章:SpringBoot整合Xxl-job实现定时任务、Linux - Linux安装部署xxl-job、Java – XXL-JOB分布式任务调度平台
XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
单机并且定时任务不多时,可选择Timer注解@Scheduled或者Cron工具类等方式来实现,但是这时候定时任务会写死在代码中,一旦启动,就不能暂停或者修改。若需修改,整个项目都要重新编译。
部署xxl-job调度中心
(一)下载源码
xxl-job 源码地址:
GitHub - xuxueli/xxl-job: A distributed task scheduling framework.(分布式任务调度平台XXL-JOB)
2.4.1版本为例:https://github.com/xuxueli/xxl-job/archive/refs/tags/2.4.1.tar.gz
xxl-job 文档地址: 分布式任务调度平台XXL-JOB
(二)初始化数据库
找到 xxl-job 安装包xxl-job-2.4.1/doc/db/路径下的tables_xxl_job.sql文件,连接到mysql数据库,导入到mysql中
执行 sql 脚本后,会生成以下8张表:
1  | xxl_job_group  | 
(三)修改配置文件
修改server.port,服务端口号,修改数据库的URL和账号密码(xxl-job-admin 下的配置文件
application.properties)1
2
3
4
5
6
7
8# 改成自己的服务端口号
server.port=8666
server.servlet.context-path=/xxl-job-admin
# 修改数据库的URL和账号密码
spring.datasource.url=jdbc:mysql:127.0;0.1:3306/xxl_job?useUnicode=true
pring.datasource.username=root
spring.datasource.password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver然后打包(lifecycle → clean → package)上传,并运行jar包,使服务运行起来
测试:地址栏输入http://IP或者`域名:8666/xxl-job-admin/`
xxl-job-admin Web界面的账号:admin 密码:123456 (默认的情况)
因为账号密码默认在代码里写死了,可在
JobInfoControllerTest的login方法修改1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class JobInfoControllerTest extends AbstractSpringMvcTest {
private static Logger logger = LoggerFactory.getLogger(JobInfoControllerTest.class);
private Cookie cookie;
public void login() throws Exception {
MvcResult ret = mockMvc.perform(
post("/login")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("userName", "admin")
.param("password", "123456")
).andReturn();
cookie = ret.getResponse().getCookie(LoginService.LOGIN_IDENTITY_KEY);
}
}
部署SpringBoot项目
(一)引入Maven依赖
1  | <!-- xxl-job依赖,maven仓库有该依赖,不需要手动安装 -->  | 
(二)修改项目关于xxl-job的配置文件
在springboot项目的 application-dev.yml 配置文件中新增 xxl-job 配置信息
1  | # Xxl-Job分布式定时任务调度中心  | 
注意:若 accessToken 不设置最好注释掉,或输入xxl-job 默认的 default_token ,否则调用时易引起获取不到 token 的报错
1  | xxl-job registry fail, registryParam:RegistryParam{registryGroup=‘EXECUTOR’, registryKey=‘xxl-job-invoice-api’, registryValue=‘http://xx:9990/’}, registryResult:ReturnT [code=500, msg=The access token is wrong., content=null]  | 
另外需要注意的几个点:
xxl.job.admin.address是xxl-job-admin服务部署的IP地址确认执行器的注册地址正确无误。检查两个地址是否均正确录入并与执行器实际部署的地址对应。
确认执行器的注册成功。可以在
xxl-job-admin的”执行器管理“页面确认两个执行器均处于在线状态。检查任务调度配置。在任务调度配置中,确保你的任务绑定了正确的执行器,并且任务的
JobHandler名称与执行器中注册的JobHandler名称一致。在
xxl-job-admin的”任务管理”页面,找到你的任务并编辑在任务编辑页面的”基本设置”中,确认”任务路由策略”选择了”单一机器”或”故障转移”,以便指定任务绑定的执行器
在”GLUE类型”为Bean模式下,确保”JobHandler“字段的值与执行器中注册的 JobHandler 名称完全匹配
检查执行器配置。在执行器部署的服务器上,确认
xxl.job.executor.appname、xxl.job.executor.address和xxl.job.executor.ip等属性的配置与xxl-job-admin上的执行器配置一致确认执行器的appname与xxl-job-admin中的执行器配置相同
确认执行器的address和ip与xxl-job-admin中的执行器配置相同
执行器即SpringBoot项目中带有
@XxlJob("xxlJobTest")注解执行器的IP地址即SpringBoot项目中带有
@XxlJob("xxlJobTest")注解部署服务的IP地址
(三)创建配置类
1  | 
  | 
(四)创建执行器
1  | 
  | 
通过调度中心进行任务调度
执行器管理–新增执行器
根据 application-dev.yml 中 xxl-job 的配置,去填写appName,机器地址,自己命名执行器名称 。
任务管理–新增任务
执行器选择已经新增的执行器,调度类型选择
CRON,选择或输入 cron 设置定时任务执行的频率;运行模式选择BEAN,JobHandler填写执行器方法上@XxlJob注解中的value值任务调度中心发起任务调度
可以在任务管理列表每个任务的操作栏选择执行一次、启动,选择启动之后会按照配置的执行频率执行任务。
配置告警邮件通知
项目上线后,调度任务执行失败,当无告警信息时,只有再数据出现错误,进行问题定位和分析时,才会知道执行结果信息,每次出现问题,不能及时的知晓对于运维而言,很不友好。但xxl-job中具备这个能力,具体配置项参考下面详细信息。
xxl-job-admin服务配置在application.properties中针对邮件告警配置项有如下信息:
1  | ### xxl-job, email  | 
这里的配置信息,是配置一个邮件发送方信息。也就是:出现告警信息时,邮件发送者。 在任务管理里面针对任务在报警邮件栏输入邮件地址,保存即可。





