跳至主要內容

定时任务

程序员李某某大约 24 分钟

定时任务

理论

小顶堆

完全二叉树:除了最后一层,其他层节点达到最大

堆就是一颗完全二叉树,分为大顶堆、小顶堆

每个定时任务延迟时间对应小顶堆一个节点

java中没有堆的数据结构,可以通过数组和链表实现,数组效率高

  • 数组首位空置 ---- 0索引不方便计算位置
  • 后面的按每层从左向右的顺序依次存放
  • 当找某个节点时,用索引/2取整,就能找到父节点的位置,即:子节点索引 / 2 = 父节点索引
  • 插入:尾插 + 上浮(先插入尾部,和父节点比较,小就交换)
  • 删除:当需要执行定时任务时,都是从堆顶取元素(堆顶最小),然后尾部元素放到堆顶,下沉(和小的子节点交换)

时间轮算法

  • 节点很多的时候,小顶堆性能差,每次下沉都要进行多次对比交换
  • 链表或数组实现时间轮:while-true-sleep,每个时间刻度都有一个链表或者数组,到了某时刻,遍历执行对应链表上的节点对应的任务,但是刻度的细分导致维护的链表指数上升
  • round型时间轮:在链表或数组实现的基础上,增加一个参数ronud,如,原本的刻度是24个代表24小时,一星期后13点执行,那就在设置时,13点对应的链表上再维护一个round = 6,每到一次round-1不执行,直到发现round=0时执行,但是还是需要遍历,效率低
  • 分层时间轮:年月日周时分秒,分别对应一个时间轮 ----- cron采用低就是这种算法

JDK

Timer

  • 缺点
    • 只能指定绝对时间,不能指定相对时间
    • schedule的丢任务,scheduleAtFixedRate的时间不准确都是由于底层是单线程,会阻塞
main(){
  Timer timer = new Timer();
  for(int i = 0; i < 2; i++){
    TimerTask task = new TestTimerTask("test" + i);
    timer.schedule(task,new Date(),2000);//参数:任务,执行时间,间隔时间(执行完毕后,再加间隔时间) ----- 会丢任务(任务阻塞,上个任务还没执行完,间隔时间到了,但是还在等执行完毕,导致任务丢失)
  //timer.scheduleAtFixedRate(task,new Date(),2000); //间隔时间会预设下次任务的时间,但执行时间也不准确
  }
}

class TestTimerTask extends TimerTask{
  private String name;
  public TestTimerTask(String name){
    this.name = name;
  }
  
  public void run(){
    sleep(3000);
    System.print.out("执行");
  }
}

ScheduledThreadPoolExecutor --- 线程池

main(){
  ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
  for(int i = 0; i < 2; i++){
    pool.schedule(()->{
     	sleep(3000);
    	System.print.out("执行");
    },2000,TimerUntil.Seconed);//没有间隔时间,只能执行一次,scheduleAtFixedRate方法可以传间隔时间
  }
}

Quartz

  • 任务job:Job就是想要实现的任务类,每个job必须实现org.quartz.Job接口的execute方法
  • 触发器trigger:SimpleTrigger、CronTrigger,一个job可以对应多个triger,但是一个trigger只能对应一个job
  • 调度器scheduler:任务调度器,将job和trigger整合,默认是创建了StdSchedulerFactory
main(){
  // 任务
  JobDetail job = JobBuilder.newJob(TestJob.class)
    .withIdentity("jobName1","jobGroup1") // 添加身份:名字,组名
    .build();
  // 触发器
  Trigger trigger = TriggerBuilder.newTrigger()
    .withIdentity("triggerName1","triggerGroup1")
    .startNow()	// 马上启动触发器
    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
    	.withIntervalInSeconds(1) // 间隔
    	.repeatForever()) //重复
    .build();
  // 调度器,从工厂获取,默认是new StdSchedulerFactory()
  Scheduler scheduler = SchedulerFactory.getDefaultScheduler();
  scheduler.scheduleJob(job,trigger);	// 关联
  scheduler.start();									// 启动
    
}

public class TestJob implements Job{
  public void execute(JobExecutionContext context) throws JobExecutionException{
    Thread.sleep(3000);
    sout("执行" + new Date())}
}
  • 包 --- quartz和quartz-jobs

Job

  • job的生命周期:每次调度器执行job时,调用execute方法前,创建一个新的job实例,即实现类的构造器先执行,execute执行完,关联的job对象会被释放,等待GC

  • JobDetail是Job的封装,可以拿到job设置等属性信息

    jobDetail.getKey().getName(); // 获取设置等job名
    
  • JobExecutionContext是运行时环境信息的容器,可以拿到job、trigger等信息

    context.getJobDetail();	// 获取jobDetail
    context.getTrigger();		// 获取trigger
    
  • JobDataMap实现了Map

    JobDetail job = JobBuilder.newJob(TestJob.class)
        .withIdentity("jobName1","jobGroup1")
      	.usingJobData("message","打印日志......")
        .build();
    
    context.getJobDetail().getJobDataMap().getString("message")
    
    • 属性注入,实现类中提供setter方法,使用usingJobData会调用set方法,并注入
    • jobDetail和trigger注入同名(k相同)时,trigger覆盖jobDetail
    JobDetail job = JobBuilder.newJob(TestJob.class)
        .withIdentity("jobName1","jobGroup1")
      	.usingJobData("format","打印日志......")
        .build();
    
    
    public class TestJob implements Job{
      private String message;
      public void setMessage(String message){
        this.message = message;
      }
      @Override
      public void execute(JobExecutionContext context) throws JobExecutionException{
        System.print.out(message);
      }
    }
    
  • 无状态的Job:每次使用新的Job,新的JobDataMap

  • 有状态的Job:实现类上加注解@PersistJobDataAfterExecution,会对job持久化,保存JobDataMap信息,可以实现累加

  • 禁止并发执行:@DisallowConcurrentExecution

Trigger

  • 触发器

  • SimpleTrigger对于设置和使用是最为简单的一种QuartzTrigger,可设置特定的日期/时间启动,且以一个可能的间隔时间重复执行

    需要注意的点

    • SimpleTrigger的属性有:开始时间、结束时间、重复次数和重复的时间间隔

    • 重复次数属性的值可以为O、正整数、或常量SimpleTrigger.REPEAT_INDEFINITELY

    • 重复的时间间隔属性值必须为大于0或长整型的正整数,以毫秒作为时间单位

    • 如果有指定结束时间属性值,则结束时间属性优先于重复次数属性,这样的好处在于:当我们需要创建一个每间隔10秒钟触发一次直到指定的结束时间的tigger,而无需去计算从开始到结束的所重复的次数,我们只需简单的指定结束时间和使用REPEAT_INDEFINITELY作为重复次数的属性值即可

  • CronTrigger

    • cron表达式和shell中的基本一致

Scheduler

  • SchedulerFactory.getDefaultScheduler()底层是

    StdSchedulerFactory stdSchedulerFactory = new StdSchedulerFactory();
    Scheduler scheduler = stdSchedulerFactory.getScheduler()
  • start启动、standby挂起、start再启动、、shutdown关闭、shutdown(true)优雅关闭

配置文件

【quartz.propertitis】

## Default Properties file for use by StdSchedulerFactory
## to create a Quartz Scheduler Instance, if a different
## properties file is not explicitly specified.
#
## ===========================================================================
## Configure Main Scheduler Properties 调度器属性
## ===========================================================================
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
org.quartz.scheduler.instanceid:AUTO
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
## ===========================================================================  
## Configure ThreadPool 线程池属性  
## ===========================================================================
#线程池的实现类(一般使用SimpleThreadPool即可满足几乎所有用户的需求)
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
#指定线程数,至少为1(无默认值)(一般设置为1-100直接的整数合适)
org.quartz.threadPool.threadCount: 10
#设置线程的优先级(最大为java.lang.Thread.MAX_PRIORITY 10,最小为Thread.MIN_PRIORITY 1,默认为5)
org.quartz.threadPool.threadPriority: 5
#设置SimpleThreadPool的一些属性
#设置是否为守护线程
#org.quartz.threadpool.makethreadsdaemons = false
#org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
#org.quartz.threadpool.threadsinheritgroupofinitializingthread=false
#线程前缀默认值是:[Scheduler Name]_Worker
#org.quartz.threadpool.threadnameprefix=swhJobThead;
## 配置全局监听(TriggerListener,JobListener) 则应用程序可以接收和执行 预定的事件通知
## ===========================================================================
## Configuring a Global TriggerListener 配置全局的Trigger监听器
## MyTriggerListenerClass 类必须有一个无参数的构造函数,和 属性的set方法,目前2.2.x只支持原始数据类型的值(包括字符串)
## ===========================================================================
#org.quartz.triggerListener.NAME.class = com.swh.MyTriggerListenerClass
#org.quartz.triggerListener.NAME.propName = propValue
#org.quartz.triggerListener.NAME.prop2Name = prop2Value
## ===========================================================================
## Configuring a Global JobListener 配置全局的Job监听器
## MyJobListenerClass 类必须有一个无参数的构造函数,和 属性的set方法,目前2.2.x只支持原始数据类型的值(包括字符串)
## ===========================================================================
#org.quartz.jobListener.NAME.class = com.swh.MyJobListenerClass
#org.quartz.jobListener.NAME.propName = propValue
#org.quartz.jobListener.NAME.prop2Name = prop2Value
## ===========================================================================  
## Configure JobStore 存储调度信息(工作,触发器和日历等)
## ===========================================================================
## 信息保存时间 默认值60秒
org.quartz.jobStore.misfireThreshold: 60000
#保存job和Trigger的状态信息到内存中的类
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
## ===========================================================================  
## Configure SchedulerPlugins 插件属性 配置
## ===========================================================================
## 自定义插件  
#org.quartz.plugin.NAME.class = com.swh.MyPluginClass
#org.quartz.plugin.NAME.propName = propValue
#org.quartz.plugin.NAME.prop2Name = prop2Value
#配置trigger执行历史日志(可以看到类的文档和参数列表)
org.quartz.plugin.triggHistory.class = org.quartz.plugins.history.LoggingTriggerHistoryPlugin  
org.quartz.plugin.triggHistory.triggerFiredMessage = Trigger {1}.{0} fired job {6}.{5} at\: {4, date, HH\:mm\:ss MM/dd/yyyy}  
org.quartz.plugin.triggHistory.triggerCompleteMessage = Trigger {1}.{0} completed firing job {6}.{5} at {4, date, HH\:mm\:ss MM/dd/yyyy} with resulting trigger instruction code\: {9}  
#配置job调度插件  quartz_jobs(jobs and triggers内容)的XML文档  
#加载 Job 和 Trigger 信息的类   (1.8之前用:org.quartz.plugins.xml.JobInitializationPlugin)
org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.XMLSchedulingDataProcessorPlugin
#指定存放调度器(Job 和 Trigger)信息的xml文件,默认是classpath下quartz_jobs.xml
org.quartz.plugin.jobInitializer.fileNames = my_quartz_job2.xml  
#org.quartz.plugin.jobInitializer.overWriteExistingJobs = false  
org.quartz.plugin.jobInitializer.failOnFileNotFound = true  
#自动扫描任务单并发现改动的时间间隔,单位为秒
org.quartz.plugin.jobInitializer.scanInterval = 10
#覆盖任务调度器中同名的jobDetail,避免只修改了CronExpression所造成的不能重新生效情况
org.quartz.plugin.jobInitializer.wrapInUserTransaction = false
## ===========================================================================  
## Sample configuration of ShutdownHookPlugin  ShutdownHookPlugin插件的配置样例‹
## ===========================================================================
#org.quartz.plugin.shutdownhook.class = \org.quartz.plugins.management.ShutdownHookPlugin
#org.quartz.plugin.shutdownhook.cleanShutdown = true
#
## Configure RMI Settings 远程服务调用配置
#
#如果你想quartz-scheduler出口本身通过RMI作为服务器,然后设置“出口”标志true(默认值为false)。
#org.quartz.scheduler.rmi.export = false
#主机上rmi注册表(默认值localhost)
#org.quartz.scheduler.rmi.registryhost = localhost
#注册监听端口号(默认值1099)
#org.quartz.scheduler.rmi.registryport = 1099
#创建rmi注册,false/never:如果你已经有一个在运行或不想进行创建注册
## true/as_needed:第一次尝试使用现有的注册,然后再回来进行创建
## always:先进行创建一个注册,然后再使用回来使用注册
#org.quartz.scheduler.rmi.createregistry = never
#Quartz Scheduler服务端端口,默认是随机分配RMI注册表
#org.quartz.scheduler.rmi.serverport = 1098
#true:链接远程服务调度(客户端),这个也要指定registryhost和registryport,默认为false
## 如果export和proxy同时指定为true,则export的设置将被忽略
#org.quartz.scheduler.rmi.proxy = false

监听器

  • 注册

    // 注册全局的JobListener
    scheduler.getListenerManager().addJobListener(new JobListener(){
    	//重写方法
    },EverythingMatcher.allJobs());
    
    // 注册局部的JobListener
    scheduler.getListenerManager().addJobListener(new JobListener(){
    	//重写方法
    },KeyMatcher.keyEquals(JobKey.jobKey("job1","group1")));
    
  • JobListener实现类重写以下方法

    • getName方法:用于获取该jobListener的名称
    • jobToBeExecuted方法:Scheduler在obDetail将要被执行时调用这个方法
    • jobExecutionVetoed方法:Scheduler在jobDetail即将被执行,但又被TriggerListerne否决时会调用该方法
    • jobWasExecuted方法:Scheduler在jobDetail被执行之后调用这个方法
  • TriggerListerner实现类重写以下方法

    • getName方法:用于获取触发器的名称
    • triggerFired方法:当与监听器相关联的Trigger被触发,Job上的execute方法将被执行时,Scheduler就调用该方法。
    • vetoJobExecution方法:在Trigger触发后,Job将要被执行时由Scheduler调用这个方法。TriggerListener 给了一个选择去否决job的执行。假如这个方法返回true,这个Job将不会为此次Trigger触发而得到执行
    • triggerMisfired方法:Scheduler调用这个方法是在Trigger错过触发时。你应该关注此方法中持续时j间长的逻辑:在出现许多错过触发的Trigger时,长逻辑会导致骨牌效应。你应当保持这上方法尽量的小
    • triggerComplete方法:Trigger被触发并且完成了Job的执行时,Scheduler调用这个方法
  • ScheduledListener

    • jobScheduled方法:用于部署jobDetail时调用

    • jobUnscheduled)方法:用于卸载jobDetail时调用

    • triggerFinalized)方法:当一个Trigger来到了再也不会触发的状态时调用这个方法.除非这个Job已设置成了持久性,否则它就会从Scheduler中移除

    • triggersPaused方法:Scheduler调用这个方法是发生在一个Trigger或Trigger组被暂停时.假如是Trigger 组的话,triggerName参数将为null

    • triggersResumed方法:Scheduler调用这个方法是发生成一个Trigger或Trigger组从暂停中恢复时.假如是

      Trigger组的话,假如是Trigger组的话,triggerName参数将为null.参数将为null

    • jobsPaused方法:当一个或一组JobDetail暂停时调用这个方法

    • jobsResumed方法:当一个或一组ob从暂停上恢复时调用这个方法.假如是一个Job组,jobName参数将为 null

    • schedulerError方法:在Scheduler的正常运行期间产生一个严重错误时调用这个方法

    • schedulerStarted方法:当Scheduler开启时,调用该方法

    • schedulerlnStandbyMode方法:当Schedulers处于StandBy模式时,调用该方法

    • schedulerShutdown方法:当Scheduler停止时,调用该方法

    • schedulingDataCleared方法:当Scheduler中的数据被清除时,调用该方法

springboot整合

  • 依赖

          <!--引入quartz定时框架-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-quartz</artifactId>
            </dependency>
    
  • 配置文件

    #quartz相关属性配置
    spring:
      quartz:
        properties:
          org:
            quartz:
              scheduler:
                #scheduler实例名称与id分配
                instanceName: clusteredScheduler
                instanceId: AUTO
                #job的保存设置
    ##          jobStore:
    ##            class: org.quartz.impl.jdbcjobstore.JobStoreTX
    ##            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
    ##            tablePrefix: QRTZ_
    ##            isClustered: true
    ##            clusterCheckinInterval: 10000
    ##            useProperties: false
              threadPool:
                class: org.quartz.simpl.SimpleThreadPool
                threadCount: 10
                threadPriority: 5
                threadsInheritContextClassLoaderOfInitializingThread: true
        #数据库方式,默认使用memory的方式(此时不需要配置jobStore)
    ##    job-store-type: jdbc
        #初始化表结构
    ##    jdbc:
    ##      initialize-schema: never
    
  • 任务

    public class MyTask extends QuartzJobBean {
        @Override
        protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            //TODO 这里写定时任务的执行逻辑
            System.out.println("简单的定时任务执行时间:"+new Date().toLocaleString());
        }
    }
    
  • 配置类

    @Configuration
    public class QuartzConfig {
    	//指定具体的定时任务类
        @Bean
        public JobDetail uploadTaskDetail() {
            return JobBuilder.newJob(MyTask.class).withIdentity("MyTask").storeDurably().build();
        }
    
        @Bean
        public Trigger uploadTaskTrigger() {
            //TODO 这里设定执行方式
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("*/5 * * * * ?");
            // 返回任务触发器
            return TriggerBuilder.newTrigger().forJob(uploadTaskDetail())
                    .withIdentity("MyTask")
                    .withSchedule(scheduleBuilder)
                    .build();
        }
    }
    
  • 问题:代码侵入高

实现定时调度任务的动态暂停,修改,启动,单次执行等操作

  • 第一步 创建一个定时任务相关实体类用于保存定时任务相关信息到数据库当中

    public class QuartzBean {
        /** 任务id */
        private String  id;
    
        /** 任务名称 */
        private String jobName;
    
        /** 任务执行类 */
        private String jobClass;
    
        /** 任务状态 启动还是暂停*/
        private Integer status;
    
        /** 任务运行时间表达式 */
        private String cronExpression;
        //省略getter setter
      }
    
  • 第二步 创建定时任务暂停,修改,启动,单次启动工具类

    public class QuartzUtils {
    
        /**
         * 创建定时任务 定时任务创建之后默认启动状态
         * @param scheduler   调度器
         * @param quartzBean  定时任务信息类
         * @throws Exception
         */
        public static void createScheduleJob(Scheduler scheduler, QuartzBean quartzBean){
            try {
                //获取到定时任务的执行类  必须是类的绝对路径名称
                //定时任务类需要是job类的具体实现 QuartzJobBean是job的抽象类。
                Class<? extends Job> jobClass = (Class<? extends Job>) Class.forName(quartzBean.getJobClass());
                // 构建定时任务信息
                JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(quartzBean.getJobName()).build();
                // 设置定时任务执行方式
                CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(quartzBean.getCronExpression());
                // 构建触发器trigger
                CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(quartzBean.getJobName()).withSchedule(scheduleBuilder).build();
                scheduler.scheduleJob(jobDetail, trigger);
            } catch (ClassNotFoundException e) {
                System.out.println("定时任务类路径出错:请输入类的绝对路径");
            } catch (SchedulerException e) {
                System.out.println("创建定时任务出错:"+e.getMessage());
            }
        }
    
        /**
         * 根据任务名称暂停定时任务
         * @param scheduler  调度器
         * @param jobName    定时任务名称
         * @throws SchedulerException
         */
        public static void pauseScheduleJob(Scheduler scheduler, String jobName){
            JobKey jobKey = JobKey.jobKey(jobName);
            try {
                scheduler.pauseJob(jobKey);
            } catch (SchedulerException e) {
                System.out.println("暂停定时任务出错:"+e.getMessage());
            }
        }
    
        /**
         * 根据任务名称恢复定时任务
         * @param scheduler  调度器
         * @param jobName    定时任务名称
         * @throws SchedulerException
         */
        public static void resumeScheduleJob(Scheduler scheduler, String jobName) {
            JobKey jobKey = JobKey.jobKey(jobName);
            try {
                scheduler.resumeJob(jobKey);
            } catch (SchedulerException e) {
                System.out.println("启动定时任务出错:"+e.getMessage());
            }
        }
    
        /**
         * 根据任务名称立即运行一次定时任务
         * @param scheduler     调度器
         * @param jobName       定时任务名称
         * @throws SchedulerException
         */
        public static void runOnce(Scheduler scheduler, String jobName){
            JobKey jobKey = JobKey.jobKey(jobName);
            try {
                scheduler.triggerJob(jobKey);
            } catch (SchedulerException e) {
                System.out.println("运行定时任务出错:"+e.getMessage());
            }
        }
    
        /**
         * 更新定时任务
         * @param scheduler   调度器
         * @param quartzBean  定时任务信息类
         * @throws SchedulerException
         */
        public static void updateScheduleJob(Scheduler scheduler, QuartzBean quartzBean)  {
            try {
                //获取到对应任务的触发器
                TriggerKey triggerKey = TriggerKey.triggerKey(quartzBean.getJobName());
                //设置定时任务执行方式
                CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(quartzBean.getCronExpression());
                //重新构建任务的触发器trigger
                CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
                trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
                //重置对应的job
                scheduler.rescheduleJob(triggerKey, trigger);
            } catch (SchedulerException e) {
                System.out.println("更新定时任务出错:"+e.getMessage());
            }
        }
    
        /**
         * 根据定时任务名称从调度器当中删除定时任务
         * @param scheduler 调度器
         * @param jobName   定时任务名称
         * @throws SchedulerException
         */
        public static void deleteScheduleJob(Scheduler scheduler, String jobName) {
            JobKey jobKey = JobKey.jobKey(jobName);
            try {
                scheduler.deleteJob(jobKey);
            } catch (SchedulerException e) {
                System.out.println("删除定时任务出错:"+e.getMessage());
            }
        }
    }
    
  • 第三步 创建一个定时任务和相关测试类

    public class MyTask1 extends QuartzJobBean {
    
        //验证是否成功可以注入service   之前在ssm当中需要额外进行配置
        @Autowired
        private AccountService service;
        @Override
        protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            Account account = new Account();
            account.setId(1);
            account = service.findByAccount(account);
            System.out.println(account.toString());
            //TODO 这里写定时任务的执行逻辑
            System.out.println("动态的定时任务执行时间:"+new Date().toLocaleString());
        }
    }
    
  • 新建一个测试Controller

    @Controller
    @RequestMapping("/quartz/")
    public class QuartzController {
        //注入任务调度
        @Autowired
        private Scheduler scheduler;
    
        @RequestMapping("/createJob")
        @ResponseBody
        public String  createJob(QuartzBean quartzBean)  {
            try {
                //进行测试所以写死
                quartzBean.setJobClass("com.hjljy.blog.Quartz.MyTask1");
                quartzBean.setJobName("test1");
                quartzBean.setCronExpression("*/10 * * * * ?");
                QuartzUtils.createScheduleJob(scheduler,quartzBean);
            } catch (Exception e) {
                return "创建失败";
            }
            return "创建成功";
        }
    
        @RequestMapping("/pauseJob")
        @ResponseBody
        public String  pauseJob()  {
            try {
                QuartzUtils.pauseScheduleJob (scheduler,"test1");
            } catch (Exception e) {
                return "暂停失败";
            }
            return "暂停成功";
        }
    
        @RequestMapping("/runOnce")
        @ResponseBody
        public String  runOnce()  {
            try {
                QuartzUtils.runOnce (scheduler,"test1");
            } catch (Exception e) {
                return "运行一次失败";
            }
            return "运行一次成功";
        }
    
        @RequestMapping("/resume")
        @ResponseBody
        public String  resume()  {
            try {
    
                QuartzUtils.resumeScheduleJob(scheduler,"test1");
            } catch (Exception e) {
                return "启动失败";
            }
            return "启动成功";
        }
    
        @RequestMapping("/update")
        @ResponseBody
        public String  update(QuartzBean quartzBean)  {
            try {
                //进行测试所以写死
                quartzBean.setJobClass("com.hjljy.blog.Quartz.MyTask1");
                quartzBean.setJobName("test1");
                quartzBean.setCronExpression("10 * * * * ?");
                QuartzUtils.updateScheduleJob(scheduler,quartzBean);
            } catch (Exception e) {
                return "启动失败";
            }
            return "启动成功";
        }
    }
    

1 springboot2.0后默认添加了quartz的依赖,可以少些很多配置信息,只需要写好自己的任务类(需要实现job类)然后通过调度器scheduler添加任务就可以了

2 通过@Bean注解简单创建定时任务的时候,直接写任务类的class就可以,但是通过scheduler的时候需要写绝对名称

3 在quartz任务暂停之后再次启动时,会立即执行一次,在更新之后也会立即执行一次

4 在springboot当中默认quartz线程池大小为10

5 在启动项目初始化时需要将项目的定时任务也进行初始化。这样比较方便不用依次进行启动

spring task

@EnableScheduling:标注启动定时任务。

@Scheduled(fixedRate = 1000 * 30) 定义某个定时任务。

@Component
@Configurable
@EnableScheduling
public class ScheduledTasks{

    @Scheduled(fixedRate = 1000 * 30)
    public void reportCurrentTime(){
        System.out.println ("Scheduling Tasks Examples: The time is now " + dateFormat ().format (new Date ()));
    }

    //每1分钟执行一次
    @Scheduled(cron = "0 */1 *  * * * ")
    public void reportCurrentByCron(){
        System.out.println ("Scheduling Tasks Examples By Cron: The time is now " + dateFormat ().format (new Date ()));
    }

    private SimpleDateFormat dateFormat(){
        return new SimpleDateFormat ("HH:mm:ss");
    }
    
}

xxl-job

官方文档open in new window

调度中心

半docker安装

  • 下载源码

    源码仓库地址Release Download
    https://github.com/xuxueli/xxl-jobDownloadopen in new window
    http://gitee.com/xuxueli0323/xxl-jobDownloadopen in new window
  • 初始化数据库

    • 执行源码中的sql文件/xxl-job/doc/db/tables_xxl_job.sql
    • 调度中心支持集群部署,集群情况下各节点务必连接同一个mysql实例
    • 如果mysql做主从,调度中心集群节点务必强制走主库
  • 编译源码

  • 配置调度中心 --- /xxl-job/xxl-job-admin/src/main/secources/application.properties

#### 调度中心JDBC链接:链接地址请保持和 2.1章节 所创建的调度数据库的地址一致
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=root
spring.datasource.password=root_pwd
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#### 报警邮箱
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
spring.mail.password=xxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
#### 调度中心通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=
#### 调度中心国际化配置 [必填]: 默认为 "zh_CN"/中文简体, 可选范围为 "zh_CN"/中文简体, "zh_TC"/中文繁体 and "en"/英文;
xxl.job.i18n=zh_CN
### 调度线程池最大线程配置【必填】
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100
#### 调度中心日志表数据保存天数 [必填]:过期日志自动清理;限制大于等于7时生效,否则, 如-1,关闭自动清理功能;
xxl.job.logretentiondays=30
  • Dockerfile暴烈端口号
EXPOSE 8080
  • 部署项目

    • 编译打包,将target和Dockerfile复制到服务器

    • cd到对应的目录下

docker build -t xxl-job:1.0 .
docker images
docker run --name xxl-job -p 8090:8080 -d xxl-job:1.0
docker ps
  • 调度中心访问地址:http://localhost:8080/xxl-job-admin (该地址执行器将会使用到,作为回调地址)

    默认登录账号 “admin/123456”

全docker安装

docker pull xuxueli/xxl-job-admin
docker run -p 8080:8080 -v /tmp:/data/applogs --name xxl-job-admin  -d xuxueli/xxl-job-admin:{指定版本}

/**
* 如需自定义 mysql 等配置,可通过 "-e PARAMS" 指定,参数格式 PARAMS="--key=value  --key2=value2" ;
* 配置项参考文件:/xxl-job/xxl-job-admin/src/main/resources/application.properties
* 如需自定义 JVM内存参数 等配置,可通过 "-e JAVA_OPTS" 指定,参数格式 JAVA_OPTS="-Xmx512m" ;
*/

docker run -e PARAMS="--spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai" -p 8080:8080 -v /tmp:/data/applogs --name xxl-job-admin  -d xuxueli/xxl-job-admin:{指定版本}

执行器

  • “执行器”项目:xxl-job-executor-sample-springboot (提供多种版本执行器供选择,现以 springboot 版本为例,可直接使用,也可以参考其并将现有项目改造成执行器)

  • 作用:负责接收“调度中心”的调度并执行;可直接部署执行器,也可以将执行器集成到现有业务项目中。

  • 引入依赖 --- xxl-job-core

  • 配置文件

#### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
#### 执行器通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=
#### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=xxl-job-executor-sample
#### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
xxl.job.executor.address=
#### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
xxl.job.executor.ip=
#### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
xxl.job.executor.port=9999
#### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
#### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
xxl.job.executor.logretentiondays=30

两个端口号都要暴露

#### web port
server.port=8081
#### 执行器端口号
xxl.job.executor.port=9999
  • dockerfile
EXPOSE 8081
EXPOSE 9999
  • 执行器组件
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
    logger.info(">>>>>>>>>>> xxl-job config init.");
    XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
    xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
    xxlJobSpringExecutor.setAppname(appname);
    xxlJobSpringExecutor.setIp(ip);
    xxlJobSpringExecutor.setPort(port);
    xxlJobSpringExecutor.setAccessToken(accessToken);
    xxlJobSpringExecutor.setLogPath(logPath);
    xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
    return xxlJobSpringExecutor;
}
  • 部署项目 --- 同上

  • 在调度中心新建任务

基础配置:
    - 执行器:任务的绑定的执行器,任务触发调度时将会自动发现注册成功的执行器, 实现任务自动发现功能; 另一方面也可以方便的进行任务分组。每个任务必须绑定一个执行器, 可在 "执行器管理" 进行设置;
    - 任务描述:任务的描述信息,便于任务管理;
    - 负责人:任务的负责人;
    - 报警邮件:任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔;
触发配置:
    - 调度类型:
        无:该类型不会主动触发调度;
        CRON:该类型将会通过CRON,触发任务调度;
        固定速度:该类型将会以固定速度,触发任务调度;按照固定的间隔时间,周期性触发;
        固定延迟:该类型将会以固定延迟,触发任务调度;按照固定的延迟时间,从上次调度结束后开始计算延迟时间,到达延迟时间后触发下次调度;
    - CRON:触发任务执行的Cron表达式;
    - 固定速度:固定速度的时间间隔,单位为秒;
    - 固定延迟:固定延迟的时间间隔,单位为秒;
任务配置:
    - 运行模式:
        BEAN模式:任务以JobHandler方式维护在执行器端;需要结合 "JobHandler" 属性匹配执行器中任务;
        GLUE模式(Java):任务以源码方式维护在调度中心;该模式的任务实际上是一段继承自IJobHandler的Java类代码并 "groovy" 源码方式维护,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务;
        GLUE模式(Shell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "shell" 脚本;
        GLUE模式(Python):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "python" 脚本;
        GLUE模式(PHP):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "php" 脚本;
        GLUE模式(NodeJS):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "nodejs" 脚本;
        GLUE模式(PowerShell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "PowerShell" 脚本;
    - JobHandler:运行模式为 "BEAN模式" 时生效,对应执行器中新开发的JobHandler类“@JobHandler”注解自定义的value值;
    - 执行参数:任务执行所需的参数;     
高级配置:
    - 路由策略:当执行器集群部署时,提供丰富的路由策略,包括;
        FIRST(第一个):固定选择第一个机器;
        LAST(最后一个):固定选择最后一个机器;
        ROUND(轮询):;
        RANDOM(随机):随机选择在线的机器;
        CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。
        LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;
        LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举;
        FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;
        BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
        SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;
    - 子任务:每个任务都拥有一个唯一的任务ID(任务ID可以从任务列表获取),当本任务执行结束并且执行成功时,将会触发子任务ID所对应的任务的一次主动调度。
    - 调度过期策略:
        - 忽略:调度过期后,忽略过期的任务,从当前时间开始重新计算下次触发时间;
        - 立即执行一次:调度过期后,立即执行一次,并从当前时间开始重新计算下次触发时间;
    - 阻塞处理策略:调度过于密集执行器来不及处理时的处理策略;
        单机串行(默认):调度请求进入单机执行器后,调度请求进入FIFO队列并以串行方式运行;
        丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败;
        覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务;
    - 任务超时时间:支持自定义任务超时时间,任务运行超时将会主动中断任务;
    - 失败重试次数;支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;

模式

BEAN类模式

Bean模式任务,支持基于类的开发方式,每个任务对应一个Java类。

  • 优点:不限制项目环境,兼容性好。即使是无框架项目,如main方法直接启动的项目也可以提供支持,可以参考示例项目 “xxl-job-executor-sample-frameless”;
  • 缺点:
    • 每个任务需要占用一个Java类,造成类的浪费;
    • 不支持自动扫描任务并注入到执行器容器,需要手动注入。
1、开发一个继承自"com.xxl.job.core.handler.IJobHandler"的JobHandler类,实现其中任务方法。
2、手动通过如下方式注入到执行器容器。
XxlJobExecutor.registJobHandler("demoJobHandler", new DemoJobHandler());

BEAN方法模式

Bean模式任务,支持基于方法的开发方式,每个任务对应一个方法。

  • 优点:
    • 每个任务只需要开发一个方法,并添加”@XxlJobopen in new window”注解即可,更加方便、快速。
    • 支持自动扫描任务并注入到执行器容器。
  • 缺点:要求Spring容器环境;

基于方法开发的任务,底层会生成JobHandler代理,和基于类的方式一样,任务也会以JobHandler的形式存在于执行器任务容器中。

  • job方法
1、任务开发:在Spring Bean实例中,开发Job方法;
2、注解配置:为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。
3、执行日志:需要通过 "XxlJobHelper.log" 打印执行日志;
4、任务结果:默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果;
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {
    XxlJobHelper.log("XXL-JOB, Hello World.");
}

GLUE模式(Java)

任务以源码方式维护在调度中心,支持通过Web IDE在线更新,实时编译和生效,因此不需要指定JobHandler。

GLUE模式(Shell)

上次编辑于:
贡献者: ext.liyuanhao3