一 什么是定时任务

定时任务就是通过运行时代码实现,在某一个特定时间或者时间周期,让程序自己去进行操作的任务。这是我的理解,我记得同学项目遇到一个问题,他说一台机器,员工刷卡记录信息,员工一刷卡,信息就被记录,同时机器就会给这名员工开一个临时工号。问题就出现,第二天,这个员工来刷卡的时候,系统提示临时工号还在使用。
项目流程:
刷卡--->记录信息(MySQL校验)--->生成临时工号(MySQL存储)---->员工--->下班销毁工号
他说员工也承认下班没有刷卡销毁,那么问题就出来,这就是系统的漏洞,当他还在烦恼的看定时任务的学习的时候,我只知道定时任务,但是我没自己写过,通常企业都是古代版本的SpringMVC,JSP,有的还是比较难懂的XML配置。我看了一篇比较简单,是SpringBoot引入的定时任务,用起来比较方便,但是很容易出问题,我下面实验了。

二 Scheduled(可以看源码)

核心注解

  1. @EnableScheduling 标记定时任务开始,更简单就是自动找
  2. @Component 任务组件实例化
  3. @Scheduled() 闹钟,我的理解就是闹钟
    案例:
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.scheduling.annotation.EnableScheduling;
    @SpringBootApplication
    @EnableScheduling //标记
    public class ScheduledDemoApplication {
     public static void main(String[] args) {
         SpringApplication.run(ScheduledDemoApplication.class, args);
     }
    

}

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Component
public class CronJobs {
    private Logger log = LoggerFactory.getLogger(CronJobs.class);
    private final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
    private  Integer count = 0;
    private  Integer count2 = 0;
    @Scheduled(fixedRate = 2000)
    public void task1() {
        count = count + 1;
        log.info("这是Task-1:任务执行第" + count + "次,Task-1执行时间:" + dateFormat.format(new Date()));
    }
}

@Scheduled()日期设置

@Scheduled(fixedRate = 2000)

当你使用fixedRate的时候,单位是毫秒,2000就是2秒。
它的意思是:距离上一次执行后2秒再执行


@Scheduled(fixedDelay = 2000)

它的意思是:距离上一次执行完成后再等2秒执行下一次

@Scheduled(cron="* * * * * ?")

它的意思是:根据你设置的cron来固定日期执行

cron规则

cron表达式中各时间元素使用空格进行分割,表达式有至少6个(也可能7个)分别表示如下含义:

  1. 秒(0~59)
  2. 分钟(0~59)
  3. 小时(0~23)
  4. 天(月)(0~31,但是你需要考虑你月的天数)
  5. 月(0~11)
  6. 天(星期)(1~7 1=SUN 或 SUN,MON,TUE,WED,THU,FRI,SAT)
  7. 7.年份(1970-2099)

其中每个元素可以是一个值(如6),一个连续区间(9-12),一个间隔时间(8-18/4)(/表示每隔4小时),一个列表(1,3,5),通配符。由于"月份中的日期"和"星期中的日期"这两个元素互斥的,必须要对其中一个设置?.

  • 0 0 10,14,16 * * ? 每天上午10点,下午2点,4点

  • 0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时

  • 0 0 12 ? * WED 表示每个星期三中午12点

  • "0 0 12 * * ?" 每天中午12点触发

  • "0 15 10 ? * *" 每天上午10:15触发

  • "0 15 10 * * ?" 每天上午10:15触发

  • "0 15 10 * * ? *" 每天上午10:15触发

  • "0 15 10 * * ? 2005" 2005年的每天上午10:15触发

  • "0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发

  • "0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发

  • "0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发

  • "0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发

  • "0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发

  • "0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发

  • "0 15 10 15 * ?" 每月15日上午10:15触发

  • "0 15 10 L * ?" 每月最后一日的上午10:15触发

  • "0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发

  • "0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发

  • "0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发


懒人有懒人办法,拿去:在线生成cron
http://www.bejson.com/othertools/cron/

三 使用说明(坑)

也是闲着没事做,我用前两种方式跑任务

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

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

@Component
public class CronJobs {
    private Logger log = LoggerFactory.getLogger(CronJobs.class);
    private final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
    private  Integer count = 0;
    private  Integer count2 = 0;


    /*
     * @author: GODV
     * @date: 2020/3/29 2:11
     * @params
     * @return:
     * @@Description: TODO(每隔2秒执行一次)
     */
    @Scheduled(fixedRate = 2000)
    public void task1() {
        count = count + 1;
        log.info("这是Task-1:任务执行第" + count + "次,Task-1执行时间:" + dateFormat.format(new Date()));
    }

    /*
     * @author: GODV
     * @date: 2020/3/29 2:26
     * @params
     * @return:
     * @@Description: TODO(方法执行完毕后在等2秒执行一次)
     */
    @Scheduled(fixedDelay = 2000)
    public void task2() {
        count2 = count2 + 1;
        String  s = "";
        //这里我们可以用一个方法来区别
        for (int i = 0; i <100000 ; i++) {
            s=s+System.currentTimeMillis();
        }
        log.info("这是Task-2:任务执行第" + count2 + "次,Task-2执行时间:" + dateFormat.format(new Date()));
    }
}

看见没,我的Task-2,本来为了区别两种方式,但是发现了其中的奥秘

图片说明

这个时候Task-1已经进行过一次,虽然Task-2这种浪费JVM的做法,我以为不会耽误Task-1执行第二次,谁知道呢,不出来。好的发现了问题,它默认是单线程的跑任务,就是你不管写了几个Task-n,它都在一个线程里,例如Task-2,如果一天不运行完毕,它就不会再去执行Task-1。


应用到实际开发中,假如定时对数据库某些操作的时候,数据库连接出现卡死现象,正好你的MyBatis或者Hbermate没有设置超时响应,那么就会造成你下面所有的任务都会在这个单线程的情况下等待。

四 配合线程使用Scheduled,效果更佳

配置线程

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class ScheduleConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        // TODO Auto-generated method stub
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //核心线程池数量,方法: 返回可用处理器的Java虚拟机的数量。
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
        //最大线程数量
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors()*5);
        //线程池的队列容量
        executor.setQueueCapacity(Runtime.getRuntime().availableProcessors()*2);
        //线程名称的前缀
        executor.setThreadNamePrefix("this-excutor-");
        // setRejectedExecutionHandler:当pool已经达到max size的时候,如何处理新任务
        // CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行
        //executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new SimpleAsyncUncaughtExceptionHandler();
    }
}

50个线程,谁用谁拿 添加@Async

package cn.personloger.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

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

@Component
public class CronJobs {
    private Logger log = LoggerFactory.getLogger(CronJobs.class);
    private final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
    private  Integer count = 0;
    private  Integer count2 = 0;


    /*
     * @author: GODV
     * @date: 2020/3/29 2:11
     * @params
     * @return:
     * @@Description: TODO(每隔2秒执行一次)
     */
    @Async
    @Scheduled(fixedRate = 2000)
    public void task1() {
        count = count + 1;
        log.info("这是Task-1:任务执行第" + count + "次,Task-1执行时间:" + dateFormat.format(new Date()));
    }

    /*
     * @author: GODV
     * @date: 2020/3/29 2:26
     * @params
     * @return:
     * @@Description: TODO(方法执行完毕后在等2秒执行一次)
     */
    @Async
    @Scheduled(fixedDelay = 2000)
    public void task2() {
        count2 = count2 + 1;
        String  s = "";
        //这里我们可以用一个方法来区别
        for (int i = 0; i <100000 ; i++) {
            s=s+System.currentTimeMillis();
        }
        log.info("这是Task-2:任务执行第" + count2 + "次,Task-2执行时间:" + dateFormat.format(new Date()));
    }
}

结果

图片说明


至于Quartz与之相比的稳定性,这个我还不清楚,毕竟实际应用开发才有说话的资格。不过这个相比Quartz是非常简单的,Quartz中对应的通过bean来装载不同的定时任务,应该也是考虑了线程问题,Quartz的配置方式是通过xml来进行定时器任务的,目前我只见过xml版本的,我接触的是,我第一家家公司开发了一个采购平台,对接的苏宁商品接口,我杠杆入职那天总监就跟我说了,我们这个线上服务每天请求苏宁几百万次接口,苏宁的技术打电话问怎么回事,我第一反应就是在开发测试的时候会跑一边商品信息与本平台录入校对的,但是没想到它是定时任务去请求别人的接口的,而且好像是分秒级别的,我跟我师傅找了很多天,因为系统不是我们开发的,我师傅进去的时候,系统基本都开发完整,都已经交付运营了,平时就是维护而已。后来我去检查线上tomcat日志的时候,本来是检查对接第三方信息平台接口信息录入,发现有个定时任务一直在跑,我不懂,我页不敢说,后来我师傅发现了,就问我注意到没有,我说我早就注意到了,还把前两天的日志给他看了,他说为什么不问他,我说尝试去Quartz的配置文件去找bean,我没找到,然后就忘记这一茬了。。。。
这一算个坑吧,定时任务去跑接口再正常不过了,就怕项目换了人,都能搞死人。。。