定义任务概述

解释

通过时间表达式这一方式来进行任务调度的被称为定时任务

分类

单机定时任务

1、单机的容易实现,但应用于集群环境做分布式部署,就会带来重复执行
  2、解决方案有很多比如加锁、数据库等,但是增加了很多非业务逻辑

分布式定时任务

1、把需要处理的计划任务放入到统一的平台,实现集群管理调度与分布式部署的定时任务 叫做分布式定时任务
  2、支持集群部署、高可用、并行调度、分片处理等

常见单机定时任务

Java自带的java.util.Timer类

package com.timer;

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/**
 * @Author: li.wang
 * @Des:
 * @Date: 2022-07-25 14:10
 */

public class MyDemoTimerTask {

    public static void main(String[] args) {

        // 定义一个任务

        TimerTask timerTask = new TimerTask() {

            @Override
            public void run() {
                System.out.println("the task is running at:" + new Date());
            }
        };

        // 计时器
        Timer timer = new Timer();
        /** 添加执行任务(延迟 1s 执行,每 2s 执行一次) */
        timer.schedule(timerTask, 1000, 2000);
        /** new Date()时间之后,每秒执行一次 -- 时间可自行指定  */
        timer.schedule(timerTask, new Date(), 1000);
        /** 当前时间之后,每秒执行一次 -- 默认当前时间之后  */
        timer.schedule(timerTask,1000);
        /** new Date()时间执行一次 -- new Date()可换成其他指定时间且 只会执行一次 */
        timer.schedule(timerTask,new Date());
        /** 取消timer指定的调度 */
        timer.cancel();

    }

}

PS:

Timer 是 JDK 自带的定时任务执行类
 多任务时,存在任务延迟执行的bug,不太推荐使用

基于线程池的ScheduledExecutorService

package com.schedule;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @Author: li.wang
 * @Des:
 * @Date: 2022-07-25 14:29
 */
public class DemoScheduledExecutorService {

    public static void main(String[] args) {

        /** 创建任务调度线程池 */
        ScheduledExecutorService scheduledExecutorService =
                Executors.newScheduledThreadPool(10);

        DemoRunTask demoRunTask = new DemoRunTask();

        /** 分配任务 -- 2s 后开始执行,每 3s 执行一次 ,与 scheduleWithFixedDelay 方法参数与功能一致
         *  demoRunTask 实际执行任务的类
         *  var2 延迟多久后执行,例如本例 2
         *  var4 执行周期 ,例如本例 3
         *  TimeUnit var2与var4使用单位
         * */
        scheduledExecutorService.scheduleAtFixedRate(demoRunTask, 2, 3, TimeUnit.SECONDS);

        /** 分配任务 -- 2s 后开始执行,只执行一次
         *  demoRunTask 实际执行任务的类
         *  var2 延迟多久后执行,例如本例 2
         *  TimeUnit var2
         * */
        scheduledExecutorService.schedule(demoRunTask,2,TimeUnit.SECONDS);
        
        
        /** 分配任务 -- 2s 后开始执行,每 3s 执行一次
         *  demoRunTask 实际执行任务的类
         *  var2 延迟多久后执行,例如本例 2
         *  var4 执行周期 ,例如本例 3
         *  TimeUnit var2与var4使用单位
         * */
        scheduledExecutorService.scheduleWithFixedDelay(demoRunTask, 2, 3, TimeUnit.SECONDS);

    }
}

PS:

ScheduledExecutorService 也是 JDK 1.5 自带的 API
 较为容易上手,且各任务间无论是 异常 还是 执行时间 均不会相互影响

SpringBoot使用注解方式开启定时任务

启动类添加注解 - @EnableScheduling

package com.sctech.demo;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
 * @author li.wang
 */
@SpringBootApplication
@EnableScheduling
public class IotApplication {
    public static void main(String[] args) {
        SpringApplication.run(IotApplication.class, args);
    }
}

任务类 - 注入容器并在方法添加@Scheduled注解

package com.sctech.demo.task;


import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;


@Component
public class ScheduleTask {


    @Scheduled(fixedRate = 1000 * 60)
    public void fixeOneMinuteRate() {
        System.out.println("任务已被成功调用...");
    }

    @Scheduled(cron = "0 0/30 * * * ?")
    public void fix30MinuteRate() {
        System.out.println("任务已被成功调用...");
    }
    
}

PS:

Spring Framework 自带的定时任务,亦称为 Spring Task
 Spring Boot 启动后会自动加载并执行定时任务

Quartz(支持分布式)

作为分布式时总结:
  基于db可实现高可用,但缺少了分布式的并行调度功能,不支持任务分片、没UI界面管理,并行调度、失败策略缺少

演示从数据库获取时间点列表,生成quartz调度池,并可对调度池增删改查

接口
package com.demo.control.service;

import com.demo.control.domain.TimeMst;
import java.util.List;

/**
 * @Author: li.wang
 * @Des:
 * @Date: 2022-07-23 15:16
 */
public interface IQuartzService {

    /**
     * 生成调度池
     *
     * @param list 时间调度集合
     * @return 调度任务集合
     */
    public boolean buildSchedulePool(List<TimeMst> list);

    /**
     * 向调度池增加新的调度
     *
     * @param time 时间调度集合
     * @return 调度任务集合
     */
    public boolean buildScheduleOne(TimeMst time) ;

    /**
     * 关闭调度池
     *
     * @return 调度任务对象信息
     */
    public boolean unSchedulePool();

    /**
     * 从调度池移除调度
     *
     * @param id 时间id
     * @return 调度任务对象信息
     */
    public boolean unScheduleOne(Long id);

    /**
     * 从调度池移除多个调度
     *
     * @param ids 时间id集合
     * @return 调度任务对象信息
     */
    public boolean unScheduleMulti(List<Long> ids);

    /**
     * 从调度池更新一个调度
     *
     * @param timeMst 时间
     * @return 调度任务对象信息
     */
    public boolean updateScheduleOne(TimeMst timeMst);

    /**
     * 销毁调度线程
     *
     * @return 结果
     */
    public int shutdownSchedulePool();

}
接口实现
package com.demo.control.service.impl;

import com.google.common.collect.Sets;
import com.demo.common.utils.DateUtils;
import com.demo.control.domain.TimeMst;
import com.demo.control.job.InitScheduleConstant;
import com.demo.control.job.ScheduleJob;
import com.demo.control.service.IQuartzService;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 调度池服务
 * 
 * @author li.wang
 * @date 2022-07-23
 *
 */
@Service
@Slf4j
public class QuartzServiceImpl implements IQuartzService
{

    /**  获取默认调度器   */
    private final Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

    public QuartzServiceImpl() throws SchedulerException {
    }

    @lombok.SneakyThrows
    @Override
    public boolean buildSchedulePool(List<TimeMst> list) {

        HashSet<Trigger> triggerSet = Sets.newHashSet();

        JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.class)
                .withIdentity("JobDetailName")
                .build();

        list.forEach(time -> {

            Trigger trigger = TriggerBuilder.newTrigger()
                    .usingJobData("id",time.getId())
                    .usingJobData("time",time.getTime())
                    .withIdentity("TriggerName"+ time.getId(),"group")
                    .startAt( DateUtils.parseDate(time.getTime()) )
                    //间隔一天
                    .withSchedule(SimpleScheduleBuilder.repeatHourlyForever(24))
                    .build();
            triggerSet.add(trigger);
            InitScheduleConstant.Id2TriggerKeyMap.put(time.getId(),trigger.getKey());
        });
        // true代表jobDetail与Trigger的Key重复则直接替换,不会抛出异常
        scheduler.scheduleJob(jobDetail,triggerSet,true);
        // 启动调度器
        scheduler.start();

        return true;
    }

    @lombok.SneakyThrows
    @Override
    public boolean buildScheduleOne(TimeMst time) {
        if(Objects.isNull(time) || StringUtils.isEmpty(time.getTime())) return false;
        //时间格式调整为当天时间
        String date = DateUtils.getDate();
        time.setTime(date + " " + time.getTime() +":00");

        JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.class)
                .withIdentity("JobDetailName"+time.getId())
                .build();

        Trigger trigger = TriggerBuilder.newTrigger()
                    .usingJobData("id",time.getId())
                    .usingJobData("time",time.getTime())
                    .withIdentity("TriggerName"+ time.getId(),"group")
                    //指定时间开始
                    .startAt( DateUtils.parseDate(time.getTime()) )
                    //从当前时间开始
                    //.startNow()
                    //到指定时间调度结束
                    //.endAt(DateUtils.parseDate( "2023-07-23 15:32:12" ))
                    //间隔一天
                    .withSchedule(SimpleScheduleBuilder.repeatHourlyForever(24))
                    //间隔30秒
                    //.withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(30))
                    .build();
        scheduler.scheduleJob(jobDetail, trigger);
        //存储调度池的触发器Key
        InitScheduleConstant.Id2TriggerKeyMap.put(time.getId(),trigger.getKey());

        // 启动调度器
        scheduler.start();

        return true;
    }

    @SneakyThrows
    @Override
    public boolean unSchedulePool() {

        if( scheduler.isStarted() && InitScheduleConstant.Id2TriggerKeyMap.keySet().size() > 0 ){
            List<TriggerKey> triggerKeys = new ArrayList<>(InitScheduleConstant.Id2TriggerKeyMap.values());
            scheduler.unscheduleJobs( triggerKeys );
            InitScheduleConstant.Id2TriggerKeyMap.clear();
        }
        return true;
    }

    @SneakyThrows
    @Override
    public boolean unScheduleOne(Long id) {

        if( scheduler.isStarted() && InitScheduleConstant.Id2TriggerKeyMap.containsKey(id) ){

            TriggerKey triggerKey = InitScheduleConstant.Id2TriggerKeyMap.get(id);
            scheduler.unscheduleJob( triggerKey );
            InitScheduleConstant.Id2TriggerKeyMap.remove(id);
        }

        return true;
    }

    @SneakyThrows
    @Override
    public boolean unScheduleMulti(List<Long> ids) {

        ids.forEach(this::unScheduleOne);
        return true;
    }

    @Override
    public boolean updateScheduleOne(TimeMst timeMst) {

        //移除一条旧调度
        unScheduleOne(timeMst.getId());
        //新增一条新调度
        buildScheduleOne(timeMst);
        return true;
    }

    @SneakyThrows
    @Override
    public int shutdownSchedulePool() {
        if(scheduler.isShutdown()){
            return 0;
        }
        scheduler.shutdown(true);
        return 1;
    }
}
使用的Bean-TimeMst
package com.zxkw.control.domain;

import lombok.Data;
import java.io.Serializable;

@Data
public class TimeMst implements Serializable
{
    private static final long serialVersionUID = 1L;

    /** 主键 */
    private Long id;

    /** 分次时间 */
    private String time;

}
TimeMst在mysql存储展示

java 唤醒主线程 定时轮询 java定时调度任务_定时任务


数据截图

java 唤醒主线程 定时轮询 java定时调度任务_java 唤醒主线程 定时轮询_02

实际使用的任务
package com.demo.control.job;

import com.demo.common.utils.spring.SpringUtils;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Author: wangli
 * @Des:
 * @Date: 2022-07-23 15:23
 */
@Slf4j
public class ScheduleJob implements Job {

    private SchedulingTask schedulingTask;

    public ScheduleJob() {
        log.info("ScheduleJob任务被创建");
        schedulingTask = SpringUtils.getBean(SchedulingTask.class);
    }

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {

        log.info("任务类:{}被创建执行,其任务名称为:{},触发器名称为:{},触发器本次生效时间为:{},触发器下次生效时间为:{}",
                this.getClass().getName(),jobExecutionContext.getJobDetail().getKey().getName(),
                jobExecutionContext.getTrigger().getKey().getName(),
                new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),
                jobExecutionContext.getTrigger().getNextFireTime());
        //执行实际任务
        String id = String.valueOf( jobExecutionContext.getTrigger().getJobDataMap().get("id") );
        schedulingTask.doJob(id);
        log.info("任务执行完毕,触发器名称为:{}",jobExecutionContext.getTrigger().getKey().getName());
    }

}
ScheduleJob使用的类
package com.demo.control.job;


import cn.hutool.core.date.DateUtil;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class SchedulingTask {


    /**
     * 执行任务
     * @param id 时间表主键
     */
    public void doJob(String id) {
       
     log.info("任务已被调用执行,使用的参数有:{}",id);
    }

}
字符串常量展示 - InitScheduleConstant
package com.demo.control.job;

import com.google.common.collect.Maps;
import org.quartz.TriggerKey;

import java.util.Map;

/**
 * @Author: li.wang
 * @Des:
 * @Date: 2022-07-23 18:03
 */
public class InitScheduleConstant {

    /**  存储本次所需的TriggerKey  - id -> TriggerKey */
    public static Map<Long, TriggerKey> Id2TriggerKeyMap = Maps.newConcurrentMap();
}
程序启动,初始化调度池
package com.demo.init;

import com.demo.common.utils.DateUtils;
import com.demo.common.utils.StringUtils;
import com.demo.control.domain.TimeMst;
import com.demo.control.service.IQuartzService;
import com.demo.control.service.ITimeMstService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import javax.annotation.PreDestroy;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @Author: li.wang
 * @Des:
 * @Date: 2022-07-23 14:13
 */
@Component
@Slf4j
public class ScheduleJobRunnerImpl implements ApplicationRunner {

    @Autowired
    private ITimeMstService timeMstService;

    @Autowired
    private IQuartzService quartzService;

    /**
     * 项目初始化运行
     * @param args
     * @throws Exception
     */
    @Override
    public void run(ApplicationArguments args) throws Exception {

        //项目加载完bean与service后,开始从数据库中捞取已定义的调度信息
        List<TimeMst> timeMst = timeMstService.selectTimeMstList(new TimeMst());
        String date = DateUtils.getDate();
        //时间格式调整为当天时间
        List<TimeMst> list = timeMst.stream().filter(t -> StringUtils.isNotEmpty(t.getTime()))
                .peek(t ->  t.setTime(date + " " + t.getTime() +":00")).collect(Collectors.toList());

        //生成调度池
        quartzService.buildSchedulePool(list);
        log.info("调度时间表数据{}", StringUtils.isEmpty(list) ? "为空,无需生成调度池" : "已生成调度池");
    }

    /**
     * 项目关闭时运行,此处主要是为了让任务执行完毕再关闭程序
     */
    @PreDestroy
    public void destroy() {

        log.info("程序正在销毁中,开始销毁调度池...");
        quartzService.shutdownSchedulePool();
    }

}
PS:
quartzService.buildSchedulePool(list)方法接受参数可改为接受形如 "2022-07-23 15:32:12"的字符
  串列表,方法体小调一下即可,无需从数据库捞取数据,此处不展示捞取过程