定时任务在应用开发中是不可或缺的功能。B/S结构的系统中,程序被触发只有两个渠道。一个是用户操作,当用户浏览器访问,发送对应的请求,对应的程序就会执行;另一个是系统内部触发,也就是某些不需要人工参与的系统操作,比如每天凌晨12:00固定执行的对账任务;订单的15分钟过期取消任务等等。后者就要求系统能够自行触发,这种时候就是定时任务出场的时候了。

列出以下常见的几种解决方案

方案1. spring-task的@Scheduled注解

spring提供的定时任务注解,操作简单,参考文章

解释:在类方法上加 @Scheduled(cron="*/6 * * * * ?") 注解,即可在spring中启用定时任务。当中的cron表达式稍后解释。

优势:集成简单

缺陷: 分布式环境下会出现并发问题,重复执行,比如同一个应用部署了两台机器时,会出现同时在8:00执行一个定时任务,那么就会执行两次定时,这时就需要调度。粗糙的解决方案是加锁,同时只允许一个执行,代码再进行一个状态判断,就不会造成反复执行,可以使用redis或者数据库都可以加锁。

方案2. rabbitmq的死信队列

解释:基于时间间隔的事件型任务可以采用该种方式,比如订单15分钟的过期取消,参考文章

主要思路是利用rabbitmq的所谓死信队列,将数据放入队列中,如果超过了一定时间则被转到另一个死信队列。此时监听死信队列,就可以执行任务取消的后序操作了。

因为借用了rabbitmq中间件,所以在分布式环境下也不会出现重复调用的问题。

优势:实现较为简单,事件型的触发方式只在需要时才触发,不需要频繁查询状态,降低消耗

缺陷:需要rabbitmq支持,应用场景限于时间超时

方案3. 独立的分布式定时任务调度中间件

解释:独立的定时任务调度中间件显然是解决问题的一个合理方案。即,将任务调度独立到一个调度服务中处理。那么就可以从外部来调度服务了。xxl分布式定时任务中间件。同类的产品有当当的 elastic-job

优势:生产级,成熟,功能丰富,适用于大部分场景

缺陷:需要独立部署

步骤:

1)阅读XXL的文档 https://www.xuxueli.com/xxl-job/ 有详细的步骤,以下列出简要步骤

2) 下载源码 仓库地址

3) 部署调度中心 xxl-job-admin

1. 执行数据库脚本 /xxl-job/doc/db/tables_xxl_job.sql
2. 修改application.properties 修改数据源,适合你的场景,主要是改改驱动版本
3. 将项目导入到idea中运行Application
4. 访问http://127.0.0.1:8080/xxl-job-admin/

4)部署执行器项目 xxl-job-executor-sample-springboot

直接运行XxlJobExecutorApplication

5)开发定时任务,并封装成rest风格的api,提供外部可调用的连接

6)添加定时任务配置,使用admin/123456登录admin,在任务管理中添加任务,如下图

定时任务配置这个任务指向的地址就是需要执行的任务链接。我的cron表达式定义是每隔3分钟执行一次。所以会每隔3分钟调用一次任务地址。

定时任务日志可以在调度日志里看到。

这里需要提一下的是,官方提供的spring-boot的只是例子,真正场景下也可以集成到自己的项目中,比如使用spring-cloud时,可以建一个独立项目,集成xxl-exeutor后使用feign进行服务调用。

定时任务基础-Cron表达式

cron表达式œ

定时设置需要使用到cron表达式,是源自linux操作系统crontab命令的一种定时格式字符串。

Cron表达式分成6-7子表达式,每个表达式表达一个计划。先来看一些例子

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
*/5 * * * * ? 每隔5秒执行一次
0 */1 * * * ? 每隔1分钟执行一次
0 0 5-15 * * ? 每天5-15点整点触发
0 0/3 * * * ? 每三分钟触发一次
0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发 
0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 

0 0 12 ? * WED 表示每个星期三中午12点
0 0 17 ? * TUES,THUR,SAT 每周二、四、六下午五点
0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发 
0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
0 0 23 L * ? 每月最后一天23点执行一次
0 15 10 L * ? 每月最后一日的上午10:15触发 
0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发 
0 15 10 * * ? 2005 2005年的每天上午10:15触发 
0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发 
0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发


"30 * * * * ?" 每半分钟触发任务
"30 10 * * * ?" 每小时的10分30秒触发任务
"30 10 1 * * ?" 每天1点10分30秒触发任务
"30 10 1 20 * ?" 每月20号1点10分30秒触发任务
"30 10 1 20 10 ? *" 每年10月20号1点10分30秒触发任务
"30 10 1 20 10 ? 2011" 2011年10月20号1点10分30秒触发任务
"30 10 1 ? 10 * 2011" 2011年10月每天1点10分30秒触发任务
"30 10 1 ? 10 SUN 2011" 2011年10月每周日1点10分30秒触发任务
"15,30,45 * * * * ?" 每15秒,30秒,45秒时触发任务
"15-45 * * * * ?" 15到45秒内,每秒都触发任务
"15/5 * * * * ?" 每分钟的每15秒开始触发,每隔5秒触发一次
"15-30/5 * * * * ?" 每分钟的15秒到30秒之间开始触发,每隔5秒触发一次
"0 0/3 * * * ?" 每小时的第0分0秒开始,每三分钟触发一次
"0 15 10 ? * MON-FRI" 星期一到星期五的10点15分0秒触发任务
"0 15 10 L * ?" 每个月最后一天的10点15分0秒触发任务
"0 15 10 LW * ?" 每个月最后一个工作日的10点15分0秒触发任务
"0 15 10 ? * 5L" 每个月最后一个星期四的10点15分0秒触发任务
"0 15 10 ? * 5#3" 每个月第三周的星期四的10点15分0秒触发任务
Name Required Allowed Values Allowed Special Characters
Seconds Y 0-59 , - * /
Minutes Y 0-59 , - * /
Hours Y 0-23 , - * /
Day of month Y 1-31 , - * ? / L W C
Month Y 0-11 or JAN-DEC , - * /
Day of week Y 1-7 or SUN-SAT , - * ? / L C #
Year N empty or 1970-2099 , - * /

Seconds (秒) :可以用数字0-59 表示,

Minutes(分) :可以用数字0-59 表示,

Hours(时) :可以用数字0-23表示,

Day-of-Month(天) :可以用数字1-31 中的任一一个值,但要注意一些特别的月份

Month(月) :可以用0-11 或用字符串 “JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC” 表示

Day-of-Week(每周):可以用数字1-7表示(1 = 星期日)或用字符口串“SUN, MON, TUE, WED, THU, FRI and SAT”表示

1
2
3
4
5
6
7
8
9
10
11
12
* 表示所有值; 
? 表示未说明的值,即不关心它为何值;用于占位; 
- 表示一个指定的范围; 5-7 就是在5-7之间
, 表示附加一个可能值; 
/ 符号前表示开始时间,符号后表示每次递增的值;0/5

L("last") ("last") "L" 用在day-of-month字段意思是 "这个月最后一天";用在 day-of-week字段, 它简单意思是 "7" or "SAT"。 如果在day-of-week字段里和数字联合使用,它的意思就是 "这个月的最后一个星期几" – 例如: "6L" means "这个月的最后一个星期五". 当我们用“L”时,不指明一个列表值或者范围是很重要的,不然的话,我们会得到一些意想不到的结果。 
W("weekday") 只能用在day-of-month字段。用来描叙最接近指定天的工作日(周一到周五)。例如:在day-of-month字段用“15W”指“最接近这个 月第15天的工作日”,即如果这个月第15天是周六,那么触发器将会在这个月第14天即周五触发;如果这个月第15天是周日,那么触发器将会在这个月第 16天即周一触发;如果这个月第15天是周二,那么就在触发器这天触发。注意一点:这个用法只会在当前月计算值,不会越过当前月。“W”字符仅能在 day-of-month指明一天,不能是一个范围或列表。也可以用“LW”来指定这个月的最后一个工作日。 

# 只能用在day-of-week字段。用来指定这个月的第几个周几。例:在day-of-week字段用"6#3"指这个月第3个周五(6指周五,3指第3个)。如果指定的日期不存在,触发器就不会触发。

C 指和calendar联系后计算过的值。例:在day-of-month 字段用“5C”指在这个月第5天或之后包括calendar的第一天;在day-of-week字段用“1C”指在这周日或之后包括calendar的第一天。

网上有现成的cron表达式生成器,可以选用,当然xxl中也集成了一个,生成完成请结合以上例子反复确认下,以免造成资源浪费

https://qqe2.com/cron

定时任务的基础qutarz

企业级应用开发中的定时任务开源库主要是qutarz,但由于它本身对分布式环境不够友好,而且侵入性强,所以无法直接使用。如果想要深入了解定时任务,就可以学习下qutarz。XXL等众多定时任务都基于qutarz实现。官网

Quartz is a richly featured, open source job scheduling library that can be integrated within virtually any Java application - from the smallest stand-alone application to the largest e-commerce system. Quartz can be used to create simple or complex schedules for executing tens, hundreds, or even tens-of-thousands of jobs; jobs whose tasks are defined as standard Java components that may execute virtually anything you may program them to do. The Quartz Scheduler includes many enterprise-class features, such as support for JTA transactions and clustering.

Quartz是一个功能丰富的,开源任务调度库,能够透明的集成到任何的java应用,从最小的独立应用或者最大的商务系统。Quartz能够用于创建简单或复杂的定时计划以执行累以十、百、千计的任务。这些任务可以被定义成独立的java组件来执行任何事情。Quartz定时器也支持众多企业级特性,比如支持JTA事务和集群。

主要组件

img

推荐教程:

快速教程https://jianshu.com/p/ce4c4400eea2

详细教程 https://www.w3cschool.cn/quartz_doc/