定时任务的实现方式总结

引言:定时任务的实现方式总结

定时任务的多种实现方式

定时任务(延时任务)

即定时完成的任务,或者说可以延迟一段时间完成的任务

使用场景有很多:

  1. 下订单,未支付,30min后自动取消订单
  2. 24小时候红包自动退回账户
  3. 每天凌晨自动备份
  4. 每个月跑一次脚本等等

最近我在实习的过程中也需要定时任务,因此就搜集资料整合了一下

实现方式

经过总结多方资料,有以下几种实现定时任务的方式:

  1. 死循环的普通方式
  2. 使用ScheduledExecutorService
  3. 使用DelayQueue
  4. 使用Spring的定时任务注解
  5. 使用Redis
  6. 使用定时任务框架
  7. 外部调用

【法一】死循环的方式

死循环的方式:

使用一个Map来保存任务信息,死循环反复遍历这个Map(比如使用迭代器遍历)

每次循环记录当前的毫秒值,如果当前毫秒值大于等于我设定的值,那么就去执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void wayToImplementScheduled() {
Map<String, Long> taskMap = new HashMap<>();
taskMap.put("任务1", Instant.now().plusSeconds(10).toEpochMilli());
// 执行定时任务
while (true) {
Set<String> taskKey = taskMap.keySet();
Iterator<String> it = taskKey.iterator();
while (it.hasNext()) {
String key = it.next();
// 如果设定的时间大于当前的时间,那么去执行任务
if (taskMap.get(key) <= Instant.now().toEpochMilli()) {
it.remove();
System.out.println("执行" + key);
}
}
}
}

注意:Instant是Java8提供的新的类,常见的用法如下:

1
2
3
4
5
6
//获取当前的Instant
Instant instant = Instant.now();
//将java.util.Date转换为Instant
Instant instant = Instant.ofEpochMilli(new Date().getTime());
//从字符串类型中创建Instant类型的时间
Instant instant = Instant.parse("1995-10-23T10:12:35Z");

【法二】使用ScheduledExecutorService

使用一个ScheduledExecutorService实现

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void wayToImplementScheduled() {
ScheduledExecutorService ses = Executors.newScheduledThreadPool(3);
ses.scheduleWithFixedDelay(
()->{
System.out.println("执行任务");
},
1, // 初始延迟
10, // 间隔
TimeUnit.SECONDS
);
}

【法三】使用DelayQueue

注意DelayQueue的使用:

  1. 泛型需要实现Delayed接口:这会导致需要重写两个方法
    • getDelay():返回剩余的延迟时间
    • compareTo():表明此队列的排序方式
  2. 此处的示例构造函数还传入了Runnable接口
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
42
43
44
45
46
class DelayTask implements Delayed {
long delay;
Runnable task;

public DelayTask(long delay, Runnable task) {
this.delay = Instant.now().toEpochMilli() + delay;
this.task = task;
}

// 返回剩余延迟
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(delay - Instant.now().toEpochMilli(), TimeUnit.MILLISECONDS);
}

// 排序依据
@Override
public int compareTo(Delayed o) {
if (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS) > 0) {
return 1;
} else if (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS) < 0) {
return -1;
} else {
return 0;
}
}
}

@Override
public void wayToImplementScheduled() {
DelayQueue<DelayTask> delayQueue = new DelayQueue<>();
delayQueue.add(new DelayTask(5000,()->{
System.out.println("执行任务1");
}));
delayQueue.add(new DelayTask(3000,()->{
System.out.println("执行任务2");
}));

while (!delayQueue.isEmpty()){
try {
delayQueue.take().task.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

【法四】Spring的定时任务

使用Spring的自带注解:

首先给Spring的启动类加上注解@EnableScheduling

比如这样:

1
2
3
4
5
6
7
@EnableScheduling
@SpringBootApplication
public class HynisApplication {
public static void main(String[] args) {
SpringApplication.run(HynisApplication.class, args);
}
}

然后使用@Scheduled这个注解:

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
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) // 只能用在方法或是注解上
@Retention(RetentionPolicy.RUNTIME) // 运行时依然存在
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
String CRON_DISABLED = "-";

String cron() default ""; // 可以写cron表达式

String zone() default "";

long fixedDelay() default -1L; // 延迟时间,一个毫秒值

String fixedDelayString() default "";

long fixedRate() default -1L;

String fixedRateString() default "";

long initialDelay() default -1L; // 第一次执行的延迟时间

String initialDelayString() default "";

TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}

@Scheduled注解源码可以了解到:

  1. 可以标记注解与方法
  2. 可以使用cron表达式

demo如下:

1
2
3
4
5
6
@Override
@Scheduled(cron = "*/10 * * * * *")
// cron表达式语法,每10s执行一次(秒分时天月周)
public void wayToImplementScheduled() {
System.out.println("定时任务执行:" + LocalDateTime.now());
}

【法五】使用Redis

上面的任务调度方法都是单机的方式,遇到分布式场景会歇菜(除非借助第三方工具,或是加锁)

待补充….

【法六】使用定时任务框架Quartz

待补充….

【法七】外部调用

可以使用比方说外部的一个curl,调用本机的一个接口的方式来实现定时调用

curl可以是一个我们自己实现的脚本去控制实现定时任务

定时任务的实现要注意

1、如何设置一个定时任务的执行周期

注意定时任务的性能问题,避免在一个周期内,数据无法执行完,一般取50%,或者20%的时间比较合适

2、注意改方法是否适合分布式场景

参考资料

  • 博客:十种延迟任务的实现方式(大牛的博客,我对此博客进行了一定的扩展)

  • 使用外部crontab调用本机服务的方式实现定时任务