任务调度的概述
# 1.1 什么是任务调度
在开发系统的时候,有时需要在特定的时间执行一些任务,或者周期性的执行一些任务。比如:
某电商系统需要在每天上午10点,下午3点,晚上8点发放一批优惠券。
某电商系统需要在每天凌晨0:10结算前一天的交易数据。
每天凌晨删除过期的垃圾信息。
每隔10分钟处理一次未支付的订单。
每隔10s同步MySQL中的数据到ElasticSearch
# 1.2 cron表达式
# 1.2.1 cron表达式的组成
说到任务调度我们必须掌握一个知识叫cron表达式,Corn表达式的作用是用来描述任务执行执行时机。
Cron表达式是一个字符串,通常字符串由6个域或者7个域构成,每一个域代表一个含义。语法格式:
7个域:秒 分 时 日 月 周 年
6个域:秒 分 时 日 月 周
# 1.2.2 cron表达式域的取值
# 1.2.3 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个)。如果指定的日期不存在,触发器就不会触发。
# 1.2.4 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.5 cron表达式在线测试工具
http://www.jsons.cn/quartzcheck/
某电商系统需要在每天上午10点,下午3点,晚上8点发放一批优惠券。
某电商系统需要在每天凌晨0:10结算前一天的交易数据。
每天凌晨删除过期的垃圾信息。
每隔10分钟处理一次未支付的订单。
每隔10s同步MySQL中的数据到ElasticSearch
# 1.3 任务调度的原理
说到任务调度,我们需要了解一个算法,时间轮算法;时间轮算法,是一种实现定时器的巧妙算法,任务调度等各种框架中都有用到。
时间轮算法的设计其实就是来自于时钟;
# (1) 时间轮的实例:
圆心位置表示的是当前的时间,随着时间推移, 圆心处的时间也会不断跳动。在计算机中时间轮的底层就是一个环形链表,或者环形数组,每个元素都存放一个链表,链表中封装了很多定时任务;
# (2) 时间轮执行任务流程;
我只需要把任务放到它需要被执行的时刻,然后等着时针转到这个时刻时,取出该时刻放置的任务,执行就可以了。对于时间轮算法,主要的功能有4个 。1. 加入任务 2. 执行任务 3. 删除任务 4. 沿着时间刻度前进;
对于这4个功能来说最复杂的功能就是加入任务。下面我们重点来讲解加入任务;
eg:初始的时候, 时间轮的指针定格在0。此时添加一个延迟时间为2s的任务, 那么这个任务将会插入到第二个时间格中;当时间轮的指针指到第二个时间格就会触发任务;
如果这个时候又插入一个延时时间为8s的任务进来, 这个任务的触发时间就是在当前时间2s的基础上加8s, 也就是10s, 那么这个任务将会插入到10s的时间格中。
# (3)"动态"时间轮
那么如果在当前时间是2s的时候, 插入一个延时时间为19s的任务时,这个任务的触发时间就是在当前时间 2s的基础上加19s, 也就是21s,在我们的时间轮上是没有21s的时间格的;这个任务将会插入到过期时间为1s的 时间格中。为什么呢?
因为当指针定格在2s的位置时, 时间格0s, 1s和2s就已经是过期的时间格。0s和1s的时间格以继续复用。
作为20s和21s的时间格;
# (4)时间轮升级
如果在当前时间是2s的时候, 插入一个延时时间为22s的任务, 这个任务的触发时间就是在2s的基础上加22s,也就是24s。
显然当前时间轮是无法找到过期时间格为24秒的时间格,因为当前过期时间最大的时间格才到21s。而且我们也没办法像前面那样再复用时间格,其他的时间格都还没过期呢。当前时间轮无法承载这个定时任务,那么应该怎么办呢?
# (5)层级时间轮
第二层时间轮也是由20个时间格组成, 每个时间格的跨度是20s。图中展示了每个时间格对应的过期时间范围, 我们可以清晰地看到, 第二层时间轮的第0个时间格的过期时间范围是 [0,19]。也就是说, 第二层时间轮的一个时间格就可以表示第一层时间轮的所有(20个)时间格;
第一级的时间轮走一圈,第二级的时间轮走一格。
在当前时间是2s的时候, 插入一个延时时间为22s的任务,该任务触发时间时间为24s。当第一层时间轮容纳不下时,进入第二层时间轮,并插入到过期时间为[20,39]的时间格中。
# (6) "动态"层级时间轮
当第一层时间轮的指针定格在1s时,超时时间0s的时间格就过期了。而这个时候,第二层时间轮第0个时间格的时间范围就从[0,19]分为了过期的[0],和未过期的[1,19]。而过期的[0]就会被新的过期时间[400]复用。
# (7)时间轮的降级
在当前时间是2s的时候, 插入一个延时时间为22s的任务,该任务能触发时间为24s。最后进入第二层时间轮,并插入到过期时间为[20,39]的时间格中。
当二层时间轮上的定时任务到期后,时间轮是怎么做的呢?
从图中可以看到,随着当前时间从2s继续往前推进,一直到20s的时候,总共经过了18s。此时第二层时间轮中,时间为[20-39s]的时间格上的任务到期。
原本延迟时间为24s的任务会被取出来,重新加入时间轮。此时该定时任务的延时时间从原本的22s,到现在还剩下4s(22s-18s)。最后停留在第一层时间轮超时时间为24s的时间格,也就是第4个时间格。
随着当前时间继续推进,再经过4s后,该定时任务到期被执行。