什么是任务调度
在开发系统的时候,有时需要在特定的时间执行一些任务,或者周期性的执行一些任务。比如:
- 某电商系统需要在每天上午10点,下午3点,晚上8点发放一批优惠券。
- 某电商系统需要在每天凌晨0:10结算前一天的交易数据。
- 每天凌晨删除过期的垃圾信息。
- 每隔10分钟处理一次未支付的订单。
- 每隔10s同步MySQL中的数据到ElasticSearch
cron表达式的组成
说到任务调度我们必须掌握一个知识叫cron表达式,Corn表达式的作用是用来描述任务执行执行时机。
Cron表达式是一个字符串,通常字符串由6个域或者7个域构成,每一个域代表一个含义。语法格式:
7个域:秒 分 时 日 月 周 年
6个域:秒 分 时 日 月 周
cron表达式域的取值
cron字符
* :代表所有可能的值。因此,“*”在Month中表示每个月,在Day-of-Month中表示每天,在Hours表示
每小时
- :表示指定范围。
, :表示列出枚举值。例如:在Minutes子表达式中,“5,20”表示在5分钟和20分钟触发。
/ :被用于指定增量。例如:在Minutes子表达式中,“0/15”表示从0分钟开始,每15分钟执行一
次。"3/20"表示从第三分钟开始,每20分钟执行一次。和"3,23,43"(表示第3,23,43分钟触发)的含
义一样。
? :用在Day-of-Month和Day-of-Week中,指“没有具体的值”。当两个子表达式其中一个被指定了值以
后,为了避免冲突,需要将另外一个的值设为“?”。例如:想在每月20日触发调度,不管20号是星期几,只
能用如下写法:0 0 0 20 * ?,其中最后以为只能用“?”,而不能用“*”。
L :用在day-of-month和day-of-week字串中。它是单词“last”的缩写。它在两个子表达式中的含义是
不同的。
在day-of-month中,“L”表示一个月的最后一天,一月31号,3月30号。
在day-of-week中,“L”表示一个星期的最后一天,也就是“7”或者“SAT”
如果“L”前有具体内容,它就有其他的含义了。例如:“6L”表示这个月的倒数第六天。“FRIL”表示这个月
的最后一个星期五。
注意:在使用“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" or
"FRI#3"指这个月第3个周五(6指周五,3指第3个)。如果指定的日期不存在,触发器就不会触发。
cron表达式实例
0 * * * * ? 每分钟触发一次
0 0 * * * ? 每天每1小时触发一次
0 0 10 * * ? 每天10点触发一次
0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
0 30 9 1 * ? 每月1号上午9点半
0 15 10 15 * ? 每月15日上午10:15触发
*/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触发
任务调度的原理
说到任务调度,我们需要了解一个算法,时间轮算法;时间轮算法,是一种实现定时器的巧妙算法,任务调度等各种框架中都有用到。
时间轮算法的设计其实就是来自于时钟;
(1) 时间轮的实例:
圆心位置表示的是当前的时间,随着时间推移, 圆心处的时间也会不断跳动。在计算机中时间轮的底层就是一个环形链表,或者环形数组,每个元素都存放一个链表,链表中封装了很多定时任务;
(2) 时间轮执行任务流程;
我只需要把任务放到它需要被执行的时刻,然后等着时针转到这个时刻时,取出该时刻放置的任务,执行就可以了。对于时间轮算法,主要的功能有4个 。1. 加入任务 2. 执行任务 3. 删除任务 4. 沿着时间刻度前进;
对于这4个功能来说最复杂的功能就是加入任务。下面我们重点来讲解加入任务;
eg:初始的时候, 时间轮的指针定格在0。此时添加一个延迟时间为2s的任务, 那么这个任务将会插入到第二个时间格中;当时间轮的指针指到第二个时间格就会触发任务;
如果这个时候又插入一个延时时间为8s的任务进来, 这个任务的触发时间就是在当前时间2s的基础上加8s, 也就是10s, 那么这个任务将会插入到10s的时间格中。
(三)"动态"时间轮
那么如果在当前时间是2s的时候, 插入一个延时时间为19s的任务时,这个任务的触发时间就是在当前时间 2s的基础上加19s, 也就是21s,在我们的时间轮上是没有21s的时间格的;这个任务将会插入到过期时间为1s的 时间格中。为什么呢?
因为当指针定格在2s的位置时, 时间格0s, 1s和2s就已经是过期的时间格。0s和1s的时间格以继续复用。作为20s和21s的时间格;
(四)时间轮升级
如果在当前时间是2s的时候, 插入一个延时时间为22s的任务, 这个任务的触发时间就是在2s的基础上加22s,也就是24s。
显然当前时间轮是无法找到过期时间格为24秒的时间格,因为当前过期时间最大的时间格才到21s。而且我们也没办法像前面那样再复用时间格,其他的时间格都还没过期呢。当前时间轮无法承载这个定时任务,那么应该怎么办呢?
(五)层级时间轮
第二层时间轮也是由20个时间格组成, 每个时间格的跨度是20s。图中展示了每个时间格对应的过期时间范围, 我们可以清晰地看到, 第二层时间轮的第0个时间格的过期时间范围是 [0,19]。也就是说, 第二层时间轮的一个时间格就可以表示第一层时间轮的所有(20个)时间格;
第一级的时间轮走一圈,第二级的时间轮走一格。
在当前时间是2s的时候, 插入一个延时时间为22s的任务,该任务触发时间时间为24s。当第一层时间轮容纳不下时,进入第二层时间轮,并插入到过期时间为[20,39]]的时间格中。
(五) "动态"层级时间轮
当第一层时间轮的指针定格在1s时,超时时间0s的时间格就过期了。而这个时候,第二层时间轮第0个时间格的时间范围就从[0,19]分为了过期的[0],和未过期的[1,19]。而过期的[0]就会被新的过期时间[400]复用。
(六)时间轮的降级
在当前时间是2s的时候, 插入一个延时时间为22s的任务,该任务能触发时间为24s。最后进入第二层时间轮,并插入到过期时间为[20,39]的时间格中。
当二层时间轮上的定时任务到期后,时间轮是怎么做的呢?
从图中可以看到,随着当前时间从2s继续往前推进,一直到20s的时候,总共经过了18s。此时第二层时间轮中,时间为[20-39s]的时间格上的任务到期
原本延迟时间为24s的任务会被取出来,重新加入时间轮。此时该定时任务的延时时间从原本的22s,到现在还剩下4s(22s-18s)。最后停留在第一层时间轮超时时间为24s的时间格,也就是第4个时间格。随着当前时间继续推进,再经过4s后,该定时任务到期被执行。
任务调度技术
crontab
Linux中用来定期执行任务的命令,crontab命令会每分钟定期检查是否有要执行的任务,如果有要执行的任务便会自动执行该任务。
crontab [ -u user ] { -l | -r | -e }
-u user : 可选参数,指定定时任务所属用户,默认当前用户。
-e :打开文本编辑器来设定定时任务,默认的文本编辑器是VI
-l :查看任务调度的列表
-r :删除用户的任务调度
任务调度语法
分 时 日 月 周 program
- - - - -
| | | | |
| | | | +----- 星期中星期几 (0 - 7) (星期天 为0)
| | | +---------- 月份 (1 - 12)
| | +--------------- 一个月中的第几天 (1 - 31)
| +-------------------- 小时 (0 - 23)
+------------------------- 分钟 (0 - 59)
实例
每月每天每小时的第 0 分钟执行一次 /bin/ls
0 * * * * /bin/ls
每天6点开始,每隔3小时,执行backup脚本文件
0 6/3 * * * /usr/bin/backup
每天7:50开启ssh服务
50 7 * * * /sbin/service sshd start
每天22:50关闭ssh服务
50 22 * * * /sbin/service sshd stop
演示:
每隔1分钟再控制台上打印"hello itcast"
总结:
- 优点:使用简单,容易维护。
- 缺点:支持的cron表达式并不是很强大,仅仅支持5个域;执行的任务仅限于一些脚本任务,不能满足一些复杂任务的需求;
Timer、ScheduledExecutor
Timer
在Jdk也为我们提供了任务调度相关支持,如Timer、ScheduledExecutor,下边我们了解下。
Timer 的优点在于简单易用,每个Timer对应一个线程,因此可以同时启动多个Timer并行执行多个任务,同一个
Timer中的任务是串行执行。
ScheduledExecutor
Java 5 推出了基于线程池设计的 ScheduledExecutor,其设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰
总结:
Timer 和 ScheduledExecutor 都仅能提供基于开始时间与重复间隔的任务调度,不能胜任更加复杂的调度需求。 比如,设置每月第一天凌晨1点执行任务、复杂调度任务的管理、任务间传递数据等等。
Quartz
Quartz是一个老牌的定时任务框架,目前在很多企业中目前还会用到,所以我们有必要讲解一下Quartz;
Quartz概述
Quartz 是一个功能强大的任务调度框架,它可以满足更多更复杂的调度需求,可以在JavaSE项目中使用,也可以在JavaEE的项目中使用,可以方便的和Spring,Spring Boot进行整合。是目前使用范围最广的定时任务框架。单体应用以及大型互联网项目中都可以看到他的身影。
特点:
- 方便和Spring及Spring Boot进行整合
- 支持集群
- 支持分布式
体系结构图
核心角色一共三个,Trigger,JobDetail ,Scheduler
Trigger:定义调度规则。
JobDetail:封装Job,Job和JobDetail的区别,Job负责定义任务的业务逻辑。 Job中的一些属性信息是定义在 JobDetail中(比如:name和group);
Trigger和JobDetail 注册到Scheduler,由Scheduler根据Trigger中的触发规则管理调度Job,Scheduler每次调度任务都会通过反射 实例化一个Job对象,然后调用execute方法执行任务;
Job参数传递
Job在运行时如何传递参数
Job实例是Scheduler通过反射创建的,有时候我们需要在Job运行的时候,需要传递参数,如何传递呢?
在JobDetail中定义Job运行传递的参数。
usingJobData("name","itcast")
在Job中就可以获取参数。
方式1:在通过Job的形参jobExecutionContext获取JobDetail,然后即可获取JobDetail定义参
数
String name = jobExecutionContext.getJobDetail().getJobDataMap().getString("name");
方式2: 在Job中定义成员变量,并且提供set方法,Scheduler注入JobDetail中定义参数;
private String name;
public void setName(String name) {
this.name = name;
}
定时任务并发处理
什么是定时任务的并发呢?
场景1:假如我们有一个任务每隔5秒执行一次,3秒即可执行完毕,这种情况下当前任务是不存在并发的。
场景2:假如我们有一个任务每隔5秒执行一次,7秒才能执行完毕。这个时候,在5秒~7秒的时候及10秒到12秒的时候,当前的任务存在并发执行(也就是在内存中有2个线程同时执行任务);并发会导致业务的重复执行;
本文暂时没有评论,来添加一个吧(●'◡'●)