# 任务调度

## 任务调度的基本需求

1. 可以配置触发规则，比如基于时刻、时间间隔、表达式、特殊的日期时间规则（节假日）。
2. 指定任务内容，比如执行一个脚本或者一段代码。任务和规则是解耦的。
3. 集中管理任务，持久化任务配置。不用把规则写在代码里面，可以看到所有的任务配置，方便维护。重启之后任务可以再次调度——配置文件或者配置中心。
4. 支持任务的串行执行，例如执行 A 任务后再执行 B 任务再执行 C 任务。
5. 支持多个任务并发执行，互不干扰（例如 ScheduledThreadPoolExecutor）。
6. 有自己的调度器，可以启动、中断、停止任务。
7. 容易集成到框架，例如Spring。

## 任务调度工具对比

| **层次**    | **举例**                        | **特点**                                                                           |
| --------- | ----------------------------- | -------------------------------------------------------------------------------- |
| **操作系统**  | Linux crontab，Windows 计划任务    | 只能执行简单脚本或者命令                                                                     |
| **数据库**   | MySQL、Oracle                  | 可以操作数据。不能执行 Java 代码                                                              |
| **工具**    | Kettle                        | 可以操作数据，执行脚本。没有集中配置                                                               |
| **开发语言**  | JDK Timer、ScheduledThreadPool | Timer：单线程\<br>JDK1.5 之后：ScheduledThreadPool（Cache、Fiexed、Single）:没有集中配置，日程管理不够灵活 |
| **容器**    | Spring Task、@Scheduled        | 不支持集群                                                                            |
| **分布式框架** | XXL-JOB，Elastic-Job，PowerJob  |                                                                                  |

## cron 表达式

Cron 表达式

| **位置** | **时间域** |      | **特殊值**          |
| ------ | ------- | ---- | ---------------- |
| 1      | 秒       | 0-59 | , - \* /         |
| 2      | 分钟      | 0-59 | , - \* /         |
| 3      | 小时      | 0-23 | , - \* /         |
| 4      | 日期      | 1-31 | , - \* ? / L W C |
| 5      | 月份      | 1-12 | , - \* /         |
| 6      | 星期      | 1-7  | , - \* ? / L W C |
| 7      | 年份（可选）  | 1-31 | , - \* /         |

* `星号(*)`：可用在所有字段中，表示对应时间域的每一个时刻，例如，在分钟字段时，表示“每分钟”；
* `问号(?)`：该字符只在日期和星期字段中使用，它通常指定为“无意义的值”，相当于点位符；
* `减号(-)`：表达一个范围，如在小时字段中使用“10-12”，则表示从 10 到 12 点，即 10,11,12；
* `逗号(,)`：表达一个列表值，如在星期字段中使用“MON,WED,FRI”，则表示星期一，星期三和星期五；
* `斜杠(/)`：x/y 表达一个等步长序列，x 为起始值，y 为增量步长值。如在分钟字段中使用 0/15，则表示为 0,15,30 和 45 秒，而 5/15 在分钟字段中表示 5,20,35,50，你也可以使用\*/y，它等同于 0/y；
* `L`：该字符只在日期和星期字段中使用，代表“Last”的意思，但它在两个字段中意思不同。L 在日期字段中，表示 这个月份的最后一天，如一月的 31 号，非闰年二月的 28 号；如果 L 用在星期中，则表示星期六，等同于 7。但是，如果 L 出现在星期字段里，而且在前面有一个数值 X，则表示“这个月的最后 X 天”，例如，6L 表示该月的最后星期五；
* `W`：该字符只能出现在日期字段里，是对前导日期的修饰，表示离该日期最近的工作日。例如 15W 表示离该月 15号最近的工作日，如果该月 15 号是星期六，则匹配 14 号星期五；如果 15 日是星期日，则匹配 16 号星期一；如果 15号是星期二，那结果就是 15 号星期二。但必须注意关联的匹配日期不能够跨月，如你指定 1W，如果 1 号是星期六，结果匹配的是 3 号星期一，而非上个月最后的那天。W 字符串只能指定单一日期，而不能指定日期范围；
* `LW` 组合：在日期字段可以组合使用 LW，它的意思是当月的最后一个工作日；
* `井号(#)`：该字符只能在星期字段中使用，表示当月某个工作日。如 6#3 表示当月的第三个星期五(6 表示星期五，#3 表示当前的第三个)，而 4#5 表示当月的第五个星期三，假设当月没有第五个星期三，忽略不触发；
* `C`：该字符只在日期和星期字段中使用，代表“Calendar”的意思。它的意思是计划所关联的日期，如果日期没有被关联，则相当于日历中所有日期。例如 5C 在日期字段中就相当于日历 5 日以后的第一天。1C 在星期字段中相当于星期日后的第一天。

Cron 表达式对特殊字符的大小写不敏感，对代表星期的缩写英文大小写也不敏感。

## 操作系统实现定时调度

### Linux crontab

crontab 命令是Linux的一个内置的基础命令，使用此命令可以指定一个cron表达式，让其在指定的时间内执行某个命令，是linux操作系统实现定时任务的基础方式。

基本用法如下：

```shell
crontab -e  # 进入vi编辑界面，编辑定时任务，每一行都是一个定时任务。 比如  0 2 1 4 * /usr/local/sh/sayhello.sh  在每年4月1日凌晨2点整运行脚本sayhello.sh
```

crontab 这种操作系统级别的调度任务，通常难以作为复杂的业务调度，较多情况下为操作系统服务：

1. 定时删除较久的日志，避免日志文件过大，造成磁盘使用率过高
2. 定时文件目录数据备份
3. 数据库表同步

> 建议使用 git + ansible 对定时任务进行管理

### Windows计划任务

参考：<https://blog.csdn.net/baidu\\_24752135/article/details/125742688>

## 数据库实现定时调度

数据库也提供了一些定时调度的方式，用以处理数据库实例中的一些数据。

### Oracle

参考：<https://blog.csdn.net/qq\\_25615395/article/details/79316368>

### MySQL

参考：<https://www.jianshu.com/p/91773f8375eb>

## Kettle

参考：<https://zhuanlan.zhihu.com/p/137383200>

## JDK 实现定时调度

### Timer

JDK 提供了`java.util.Timer`类用以支持定时调度，`Timer`类就是一个定时器，他提供了 `schedule()` 方法对任务进行调度：

```java
// 定时器名称为 PrintNowTime， 且作为daemon线程运行
Timer timer = new Timer("PrintNowTime", true);
// 创建任务
TimerTask timerTask = new TimerTask() {
    @Override
    public void run() {
        System.out.println(LocalDateTime.now());
    }
};

// void schedule(TimerTask task, Date time)
// 指定精确的时间执行任务
// 在2024年1月1日执行0点0分0秒执行任务
timer.schedule(timerTask, new Date(2024, Calendar.JANUARY, 1));

// void schedule(TimerTask task, long delay)
// 延时执行任务
// 30秒后执行一次任务
timer.schedule(timerTask, TimeUnit.SECONDS.toMillis(30));

// void schedule(TimerTask task, long delay, long period)
// 指定时间间隔循环执行任务，并且指定第一次执行的延时时间
// 每隔5分钟执行一次任务，第一次运行时间在10s后
timer.schedule(timerTask, TimeUnit.SECONDS.toMillis(10), TimeUnit.MINUTES.toMillis(5));
// schedule不具有追赶性：当任务延时时间很长中间任务未被执行的任务就被取消掉不执行了。
// scheduleAtFixedRate具有追赶性：当任务延时时间很长中间任务会进行补充行执行。
timer.scheduleAtFixedRate(timerTask, TimeUnit.SECONDS.toMillis(10), TimeUnit.MINUTES.toMillis(5));

// void schedule(TimerTask task, Date firstTime, long period)
// 指定时间间隔执行任务，并且指定第一次的开始时间
// 从2024年1月1日开始执行，此后每隔20秒执行一次任务
timer.schedule(timerTask, new Date(2024, Calendar.JANUARY, 1), TimeUnit.SECONDS.toMillis(20));
timer.scheduleAtFixedRate(timerTask, new Date(2024, Calendar.JANUARY, 1), TimeUnit.SECONDS.toMillis(20));

```

此外，`Timer` 还可以取消任务，被取消的定时器将会放弃所有的计划任务，但是如果当前有任务正在执行，不会干扰当前任务的运行，当前的任务将会是最后一次运行：

```java
// 取消任务
timer.cancel();

// 回收任务，让垃圾收集器回收对象，一般不用调用该方法
timer.purge();
```

Timer 是单线程的，如果一个timer设置了多个调度任务（调用了多个 `schedule()` 方法），那么多个任务之间按照触发顺序执行，这样任务的运行时间就会与定时器设置的时间有出入，并且，如果其中某个任务异常使Timer线程死掉，从而影响后去任务执行，故JDK1.5 新引入`ScheduledExecutorService`类，用于提供多线程的任务调度。

### ScheduledExecutorService

```java
//创建线程池
ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(3);
//ScheduledThreadPoolExecutor pool1 = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(3);

//开始定时任务
pool.scheduleWithFixedDelay(new TimerTask() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "---" + new Date());
    }
}, 0, 2, TimeUnit.SECONDS);
```

## Spring Task

Spring Task 内部基于`ScheduledExecutorService`，也是一种较为简单的定时任务实现方式。他提供了 `@Scheduled` 注解，用以声明式的方式定义任务调度。注意Spring Task不支持集群，集群运行任务可能会发生一些问题。

使用Spring Task：

```xml
<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
</dependency>
```

开启定时调度功能：

```java
@SpringBootApplication
@EnableScheduling
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
```

定义定时任务：

```java
@Component
public class MyTask3 {

    private int count=0;

    @Scheduled(cron="*/6 * * * * ?")
    private void process() {
        System.out.println("this is scheduler task runing  "+(count++));
    }
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yangsx95.gitbook.io/notes/domain-specific/ren-wu-diao-du.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
