Java 定时任务学习笔记

线程等待实现

创建一个线程,然后在 while 循环里一直运行,通过 sleep 方法来达到定时任务的效果

public class Task {

    public static void main(String[] args) {
        // run in a second
        final long timeInterval = 1000;
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("Hello !!");
                    try {
                        Thread.sleep(timeInterval);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

Timer 实现

JDK 自带的 Timer 是一个定时器工具,使用一个后台线程计划执行指定任务,可以安排任务“执行一次”或者定期“执行多次”

Timer 类的核心方法如下:

// 在指定延迟时间后执行指定的任务
schedule(TimerTask task,long delay);

// 在指定时间执行指定的任务。(只执行一次)
schedule(TimerTask task, Date time);

// 延迟指定时间(delay)之后,开始以指定的间隔(period)重复执行指定的任务
schedule(TimerTask task,long delay,long period);

// 在指定的时间开始按照指定的间隔(period)重复执行指定的任务
schedule(TimerTask task, Date firstTime , long period);

// 在指定的时间开始进行重复的固定速率执行任务
scheduleAtFixedRate(TimerTask task,Date firstTime,long period);

// 在指定的延迟后开始进行重复的固定速率执行任务
scheduleAtFixedRate(TimerTask task,long delay,long period);

// 终止此计时器,丢弃所有当前已安排的任务。
cancal();

// 从此计时器的任务队列中移除所有已取消的任务。
purge();

定义一个通用的 TimerTask 类,用于定义执行的任务

public class DoSomethingTimerTask extends TimerTask {

    private String taskName;

    public DoSomethingTimerTask(String taskName) {
        this.taskName = taskName;
    }

    @Override
    public void run() {
        System.out.println(new Date() + " : 任务「" + taskName + "」被执行。");
    }
}

在指定延迟时间后执行一次,这是比较常见的场景,比如:当系统初始化某个组件之后,延迟几秒,然后进行定时任务的执行

public class DelayOneDemo {

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new DoSomethingTimerTask("DelayOneDemo"),1000L);
    }
}

在指定的延迟时间开始执行定时任务,定时任务按照固定的间隔进行执行,比如:延迟 2 秒执行,固定执行间隔为 1 秒

public class PeriodDemo {

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new DoSomethingTimerTask("PeriodDemo"),2000L,1000L);
    }
}

在指定的延迟时间开始执行定时任务,定时任务按照固定的速率进行执行,比如:延迟 2 秒执行,固定速率为 1 秒

public class FixedRateDemo {

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new DoSomethingTimerTask("FixedRateDemo"),2000L,1000L);
    }
}

schedule 和 scheduleAtFixedRate 的区别在于:当某一次任务执行超时,恢复后两者都会立即执行下次任务,schedule 还是按照原来的间隔,scheduleAtFixedRate 则会加快节奏,努力追上进度


ScheduledExecutorService 实现

基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响

ScheduledExecutorService 主要有以下 4 个方法:

ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
<V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit);
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnitunit);
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnitunit);

ScheduledExecutorService 定义的这四个接口方法和 Timer 对应的方法几乎一致,只不过 Timer 的 scheduled 方法需要在外部传入一个 TimerTask 的抽象任务。ScheduledExecutorService 封装更加细致,传入 Runnable 或 Callable 内部都会封装类似 TimerTask 的抽象任务类(ScheduledFutureTask),然后传入线程池,启动线程去执行该任务

scheduleAtFixedRate 方法,按指定频率周期执行某个任务,定义及参数说明:

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
				long initialDelay,
				long period,
				TimeUnit unit);

参数对应含义:command 为被执行的线程,initialDelay 为初始化后延时执行时间,period 为两次开始执行最小间隔时间,unit 为计时单位

scheduleAtFixedRate 是以 period 为间隔来执行任务的,如果任务执行时间小于 period,则上次任务执行完成后会间隔 period 后再去执行下一次任务。但如果任务执行时间大于 period,则上次任务执行完毕后会不间隔的立即开始下次任务

public class ScheduleAtFixedRateDemo implements Runnable{

    public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        executor.scheduleAtFixedRate(
                new ScheduleAtFixedRateDemo(),
                0,
                1000,
                TimeUnit.MILLISECONDS);
    }

    @Override
    public void run() {
        System.out.println(new Date() + " : 任务「ScheduleAtFixedRateDemo」被执行。");
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

scheduleWithFixedDelay 方法,按指定频率间隔执行某个任务。定义及参数说明:

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
				long initialDelay,
				long delay,
				TimeUnit unit);

参数对应含义:command 为被执行的线程;initialDelay 为初始化后延时执行时间,period 为前一次执行结束到下一次执行开始的间隔时间(间隔执行延迟时间),unit 为计时单位

scheduleWithFixedDelay 不管任务执行多久,都会等上一次任务执行完毕后再延迟 delay 后去执行下次任务

public class ScheduleAtFixedRateDemo implements Runnable{

    public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        executor.scheduleWithFixedDelay(
                new ScheduleAtFixedRateDemo(),
                0,
                1000,
                TimeUnit.MILLISECONDS);
    }

    @Override
    public void run() {
        System.out.println(new Date() + " : 任务「ScheduleAtFixedRateDemo」被执行。");
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Spring Task

Spring 自带一套定时任务工具 Spring-Task,支持注解和配置文件两种形式

基于注解形式的实现如下:

@Component("taskJob")
public class TaskJob {

    @Scheduled(cron = "0 0 3 * * ?")
    public void job1() {
        System.out.println("通过cron定义的定时任务");
    }

    @Scheduled(fixedDelay = 1000L)
    public void job2() {
        System.out.println("通过fixedDelay定义的定时任务");
    }

    @Scheduled(fixedRate = 1000L)
    public void job3() {
        System.out.println("通过fixedRate定义的定时任务");
    }
}

Quartz 框架

Quartz 是 Job scheduling(作业调度)领域的一个开源项目,既可以单独使用,也可以和 Spring 框架整合使用。使用Quartz 可以开发一个或者多个定时任务,每个定时任务可以单独指定执行的时间,例如每隔 1 小时执行一次,每个月第一天上午 10 点执行一次、每个月最后一天下午 5 点执行一次等等

Quartz 的核心概念包括:

  • Scheduler:调度器,是 Quartz 的核心,负责管理和调度任务
  • Job:任务,是实际执行的工作单元。需要实现 Job 接口
  • JobDetail:定义任务的详细信息,包括任务的名称、组、以及任务的类
  • Trigger:触发器,定义任务何时执行。常用的触发器包括 SimpleTrigger 和 CronTrigger
  • JobStore:任务存储,定义任务的存储方式。常见的有 RAMJobStore(内存存储)和 JDBCJobStore(数据库存储)

要使用 Quartz,首先需要在项目的 pom 文件中引入相应的依赖:

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>
</dependency>

定义执行任务的 Job,这里要实现 Quartz 提供的 Job 接口:

public class HelloJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("Hello, Quartz! Current time: " + System.currentTimeMillis());
    }
}

创建 Scheduler 和 Trigger,并执行定时任务:

public class QuartzExample {
    public static void main(String[] args) throws SchedulerException {
        // 创建 Scheduler 实例
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 定义一个 JobDetail 实例
        JobDetail job = JobBuilder.newJob(HelloJob.class)
                .withIdentity("helloJob", "group1")
                .build();

        // 创建一个触发器,每隔5秒执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("helloTrigger", "group1")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(5)
                        .repeatForever())
                .build();

        // 调度任务
        scheduler.start();
        scheduler.scheduleJob(job, trigger);
    }
}

CronTrigger 允许使用 Cron 表达式来定义复杂的调度规则

public class CronTriggerExample {
    public static void main(String[] args) throws SchedulerException {
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        JobDetail job = JobBuilder.newJob(HelloJob.class)
                .withIdentity("cronJob", "group1")
                .build();

        // 使用 Cron 表达式创建触发器
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("cronTrigger", "group1")
                .withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
                .build();

        scheduler.start();
        scheduler.scheduleJob(job, trigger);
    }
}

Cron 表达式用于定义任务调度的时间规则,它由 6 或 7 个字段组成,字段之间用空格分隔,以下是每个字段的含义:

┌───────────── 秒 (0 - 59)
│ ┌───────────── 分 (0 - 59)
│ │ ┌───────────── 小时 (0 - 23)
│ │ │ ┌───────────── 日 (1 - 31)
│ │ │ │ ┌───────────── 月 (1 - 12)
│ │ │ │ │ ┌───────────── 星期几 (0 - 7) (0 和 7 都是星期日)
│ │ │ │ │ │
│ │ │ │ │ │
* * * * * *

如下特殊字符:

  • *:表示任意值
  • ?:仅在日和星期字段中使用,表示不指定值
  • -:表示范围,例如 10-12 表示从 10 到 12
  • `,:表示列表值,例如 1,2,3 表示 1、2、3
  • /:表示增量,例如 0/15 表示从 0 开始每 15 分钟
  • L:表示最后,例如 L 在日字段表示月的最后一天
  • W:表示最近的工作日,例如 15W 表示最接近 15 号的工作日
  • #:表示第几个星期几,例如 2#1 表示第一个星期一

示例:

  • 0 0 12 * * ?:每天中午 12 点执行
  • 0 15 10 ? * *:每天上午 10:15 执行
  • 0 15 10 * * ?:每天上午 10:15 执行
  • 0 15 10 * * ? 2024:2024 年每天上午 10:15 执行
  • 0 * 14 * * ?:每天下午 2 点到 2:59 每分钟执行一次
  • 0 0/5 14 * * ?:每天下午 2 点到 2:55 每 5 分钟执行一次
  • 0 0/5 14,18 * * ?:每天下午 2 点到 2:55 每 5 分钟执行一次,以及每天下午 6 点到 6:55 每 5 分钟执行一次
  • 0 0-5 14 * * ?:每天下午 2 点到 2:05 每分钟执行一次
  • 0 10,44 14 ? 3 WED:每年三月的每个星期三下午 2:10 和 2:44 执行

使用 JobListener 可以在任务执行的不同阶段进行拦截和处理

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;

public class MyJobListener implements JobListener {
    @Override
    public String getName() {
        return "MyJobListener";
    }
    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
      //在任务即将被执行时调用,可以在任务执行前进行一些准备工作或记录日志。
        System.out.println("Job is about to be executed: " + context.getJobDetail().getKey());
    }
    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
      //在任务执行被否决时调用,当某些条件满足时,可以阻止任务的执行,并在此方法中执行相应的处理逻辑。
        System.out.println("Job execution was vetoed: " + context.getJobDetail().getKey());
    }
    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
      //在任务执行完成后调用,可以在任务执行后进行一些清理工作或记录日志。如果任务执行过程中抛出异常,jobException 将包含该异常信息。
        System.out.println("Job was executed: " + context.getJobDetail().getKey());
        if (jobException != null) {
            System.out.println("Job encountered an exception: " + jobException.getMessage());
        }
    }
}

在 Scheduler 中注册 JobListener

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

public class JobListenerExample {
    public static void main(String[] args) throws SchedulerException {
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
      //创建任务
        JobDetail job = JobBuilder.newJob(HelloJob.class)
                .withIdentity("listenerJob", "group1")
                .build();
      创建触发器
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("listenerTrigger", "group1")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(5)
                        .repeatForever())
                .build();

        // 创建并注册 JobListener
        MyJobListener listener = new MyJobListener();
        scheduler.getListenerManager().addJobListener(listener);

        scheduler.start();
        scheduler.scheduleJob(job, trigger);
    }
}

Quartz 默认任务是并发执行的,如果需要确保同一个任务实例不被并发执行,可以在 Job 添加 @DisallowConcurrentExecution 注解,此时新的任务实例只有在前一个实例完成后才会开始执行

Spring Boot 集成 Quartz 的方式也很简单,引入封装好的 Quartz 依赖

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

定义执行任务的 Job 和之前没有区别,定义 Scheduler 和 Trigger

import com.example.demo.quartz.task.DongAoJob;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 定义任务描述和具体的执行时间
 */
@Configuration
public class QuartzConfig {
    @Bean
    public JobDetail jobDetail() {
        //指定任务描述具体的实现类
        return JobBuilder.newJob(HelloJob.class)
                // 指定任务的名称
                .withIdentity("dongAoJob")
                // 任务描述
                .withDescription("任务描述:用于输出冬奥欢迎语")
                // 每次任务执行后进行存储
                .storeDurably()
                .build();
    }
    
    @Bean
    public Trigger trigger() {
        //创建触发器
        return TriggerBuilder.newTrigger()
                // 绑定工作任务
                .forJob(jobDetail())
                // 每隔 5 秒执行一次 job
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5))
                .build();
    }
}

Quartz 默认采用 RAMJobStore,将任务相关信息保存在内存里,应用重启后,定时任务信息将会丢失,下面采用数据库方式存储任务信息

在 application.properties 文件中加入 Quartz 相关配置

# 将 Quartz 持久化方式修改为 jdbc
spring.quartz.job-store-type=jdbc
# 实例名称(默认为quartzScheduler)
spring.quartz.properties.org.quartz.scheduler.instanceName=SC_Scheduler
# 实例节点 ID 自动生成
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
# 修改存储内容使用的类
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
# 数据源信息
spring.quartz.properties.org.quartz.jobStore.dataSource=quartz_jobs
spring.quartz.properties.org.quartz.dataSource.quartz_jobs.driver=com.mysql.cj.jdbc.Driver
spring.quartz.properties.org.quartz.dataSource.quartz_jobs.URL=jdbc:mysql://127.0.0.1:3306/quartz_jobs?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
spring.quartz.properties.org.quartz.dataSource.quartz_jobs.user=root
spring.quartz.properties.org.quartz.dataSource.quartz_jobs.password=123456

下载 Quartz 发布包,下载完成后,解压缩进入 /docs/dbTables 目录,找到匹配数据库的 SQL 文件,下载地址:https://www.quartz-scheduler.org/downloads/,创建 quartz_jobs 数据库,执行 SQL 文件即可

在配置文件中开启分布式支持

# 开启集群,多个 Quartz 实例使用同一组数据库表
spring.quartz.properties.org.quartz.jobStore.isClustered=true

Quartz 支持分布式定时任务的原理是在数据库中配置定时器信息, 以数据库锁的方式达到同一个任务始终只有一个节点在运行

上图每个服务器节点有一个 Scheduler 实例(调度器线程),每个 Scheduler 实例会争抢访问数据库的 Trigger 并加行锁,比如一个 Scheduler 实例想要访问数据库看是否有 Trigger 将要触发,那么它就开一个事务,首先获取并占用 TRIGGER_ACCESS 锁,然后再处理业务,在此期间其他实例无法访问与该 Trigger 有关的数据表,也就无法执行任务。获得行锁的 Scheduler 实例处理完业务后 commit work,结束事务,TRIGGER_ACCESS 锁就被释放了

如果一个 Scheduler 实例获取一个 TRIGGER_ACCESS 锁但是还没处理完就挂掉了,会导致与 Trigger 有关的表一直处于加锁状态无法被其他 Scheduler 实例访问,因此 Quartz 又提出了一个接管锁的机制:

  • 每个 Scheduler 实例都在 QRTZ_SCHEDULER_STATE 表里有自己的唯一 ID,例如以 hostname + time 标识
  • 每个 Scheduler 实例的 ClusterManager 线程定期往 QRTZ_SCHEDULER_STATE 表更新 LAST_CHECKIN_TIME 作为心跳
  • 当某个 Scheduler 实例超过一定时间没有心跳更新时,其它 Scheduler 实例得到这个信息,会接管对应的行锁,并恢复过时的任务

XXL-JOB

XXL-JOB 是一个可以在 WEB 界面配置执行定时任务的中间件,支持分布式服务调用,XXL-JOB 自身也可以部署多个节点组成集群,本身是一个基于 SpringBoot 的 Java WEB 程序,可以通过下载 GitHub 源码进行部署

XXL-JOB 中“调度模块”和“任务模块”完全解耦,调度模块进行任务调度时,将会解析不同的任务参数发起远程调用,调用各自的远程执行器服务。这种调用模型类似RPC调用,调度中心提供调用代理的功能,而执行器提供远程服务的功能

基于数据库的集群方案,数据库选用Mysql;集群分布式并发环境中进行定时任务调度时,会在各个节点会上报任务,存到数据库中,执行时会从数据库中取出触发器来执行,如果触发器的名称和执行时间相同,则只有一个节点去执行此任务

通过 git 下载源码

git clone git@github.com:xuxueli/xxl-job.git

打开并执行 doc/db/tables_xxl_job.sql 文件

修改 xxl-job-admin/src/main/resources 的 application.properties 配置文件,修改数据库配置

### xxl-job, datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=your_username
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

运行 xxl-job-admin/src/main/java 包下的 XxlJobAdminApplication的main() 方法,实际为一个 springboot 项目,启动后即运行一个 web 程序

在浏览器中访问 http://localhost:8080/xxl-job-admin/toLogin ,出现如下页面,默认登录账户:admin,密码:123456,第一次登录成功后请修改密码

创建一个 SpringBoot 项目,引入 maven 依赖

<!-- xxl job -->
<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>2.4.0</version>
</dependency>

yaml 文件配置,将 addresses 修改为自己部署的 xxl-job-admin 地址

xxl:
  job:
    executor:
      appname: ${spring.application.name}
      logpath: ${spring.application.name}/xxl-job
      logretentiondays: 30
    admin:
      addresses: http://127.0.0.1:8080/xxl-job-admin

xxl-job 没有使用 spring-boot-starter,需自行将配置类注入到 spring 容器

@Configuration
public class XxlJobConfig {
    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    @Value("${xxl.job.accessToken}")
    private String accessToken;

    @Value("${xxl.job.executor.appname}")
    private String appname;

    @Value("${server.port}")
    private int port;

    @Value("${xxl.job.executor.logpath}")
    private String logPath;

    @Value("${xxl.job.executor.logretentiondays}")
    private int logRetentionDays;

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        xxlJobSpringExecutor.setPort(port + 10000);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

        return xxlJobSpringExecutor;
    }
}

在 xxl-job-admin 创建执行器,将 appname 替换为你的服务名,选择自动注册

启动客户端程序,将会自动注册到 xxl-job-admin 服务端

创建一个任务调度,在客户端中编写如下代码,并重新启动客户端

@Slf4j
@Component
public class XxlJobHandler {

    /**
     * 更新状态
     **/
    @XxlJob("UpdateStatus")
    public void updateStatus() {
        String jobParam = XxlJobHelper.getJobParam();
        log.info("任务执行" + jobParam);
    }
}

注意 @XxlJob("UpdateStatus") 注解的 UpdateStatus 表示任务的唯一名称,XxlJobHelper.getJobParam(); 可以用来获取执行任务时传递的参数

主要在 Cron 中配置任务调度的时间周期,可选择 CRON 或固定速度,JobHandler 需配置 @XxlJob 注解的名称

创建完成后,点击操作,点击执行一次,任务执行成功将会打印日志。点击启动任务,将会按照我们既定的调度周期执行任务

posted @ 2024-12-09 14:46  低吟不作语  阅读(33)  评论(0编辑  收藏  举报