springboot定时任务


title: springboot定时任务
copyright: true
categories: springmvc和springboot
tags: 配置文件
password:


  • 一、白拿拿项目中需要每天凌晨统计一次昨天一天的邀请排行榜,与定时任务有关代码示例如下:
    @Component 
    public class ScheduleHandler { 
     
        private final IChannelSourceConfigService channelSourceConfigService; 
     
        public ScheduleHandler(IChannelSourceConfigService channelSourceConfigService) { 
            this.channelSourceConfigService = channelSourceConfigService; 
        } 
     
        @Scheduled(cron = "0 7 0 * * ?") 
        public void setShowkerCountCache() { 
            channelSourceConfigService.refreshAll(); 
        } 
     
    } 
 
  • 二、当有多个定时器的时候 需要异步使用 增加定时器线程池配置
    @Configuration 
    @EnableScheduling 
    public class ScheduleConfig implements SchedulingConfigurer { 
     
        @Override 
        public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { 
            taskRegistrar.setScheduler(taskExecutor()); 
        } 
     
        @Bean(destroyMethod = "shutdown") 
        public Executor taskExecutor() { 
            return Executors.newScheduledThreadPool(100); 
        } 
    } 
 
  • 三、详细内容

    Spring 定时任务实例
    Spring 中使用定时任务很简单,只需要@EnableScheudling 注解启用即可,并不要求是一个 Spring Mvc 的项目。
    对于一个 Spring Boot 项目,使用定时任务的简单方式如下:
    pom.xml 中

    <parent> 
     <groupId>org.springframework.boot</groupId> 
     <artifactId>spring-boot-starter-parent</artifactId> 
     <version>1.5.3.RELEASE</version> 
    </parent> 
    <dependencies> 
     <dependency> 
     <groupId>org.springframework.boot</groupId> 
     <artifactId>spring-boot-starter</artifactId> 
     </dependency> 
    </dependencies>
Application.java 
    @EnableScheduling 
    @SpringBootApplication 
    public class Application { 
         public static void main(String[] args) { 
            SpringApplication.run(Application.class, args); 
         } 
    } 

@EnableScheduling 是必须的。默认时定时任务的线程是由 Executors.defaultThreadFactory() 产生的,线程名称是 "pool-NUMBER-thread-...", 关键是线程的 daemon 属性为 false, 阻止了主线程的退出,使得任务能一遍遍执行。

SchedulRunner.java

    @Component 
    public class ScheduleRunner { 
     
        @Scheduled(fixedDelay = 5000) 
        public void job1() { 
            System.out.println(Thread.currentThread() + ", job1@" + LocalTime.now()); 
        } 
    } 

顺带提一下注解@Scheduled的各个属性

  1. cron: 以 UN*X 的 cron 的方式定义 job, 如 "0 * * * * NON-FRI"
  2. fixedRate: 每次任务启动时的间隔时间,fixedRateString,意义是一样,只是可以通过外部来定义,如 fixedRateString = "${job1.fixed.rate}"
  3. fixedDelay: 上次任务结束后间隔多少时间再启动下一次任务,这样避免前一个任务尚未结束又启动下一个任务,fixedDelayString 类似 fixedRateString
  4. intialDelay: 程序启动后至任务首次执行时的间隔时间,针对 fixedRate(fixedRateString), fixedDelay(fixedDelayString)
  5. zone: 给 cron 表达式用的时区
  6. 注意, 以上的时间都是毫秒

 启动这个 Spring Boot 项目,可以看到 job1 每隔五分钟执行一次,并且全部由一个线程来执行
 Thread[pool-1-thread-1,5,main], job1@21:57:46.822
 Thread[pool-1-thread-1,5,main], job1@21:57:51.831
 Thread[pool-1-thread-1,5,main], job1@21:57:56.836
 Thread[pool-1-thread-1,5,main], job1@21:58:01.841
 居然总是同一个线程
 如果我们把上面的 fixedDelay 改成 fixedRate, 并且用 Thread.sleep(20000) 来模拟单次任务耗时 20 秒,试图让上次任务还在进行当中执行下一次任务

    @Component 
    public class ScheduleRunner { 
     
    @Scheduled(fixedRate = 5000) 
    public void job1() { 
     System.out.println(Thread.currentThread() + ", job1@" + LocalTime.now()); 
     try { 
     Thread.sleep(20000); 
     } catch (InterruptedException e) { 
     } 
     } 
    }  

 执行后,发现事与愿为
 Thread[pool-1-thread-1,5,main], job1@21:58:57.564
 Thread[pool-1-thread-1,5,main], job1@21:59:17.572
 Thread[pool-1-thread-1,5,main], job1@21:59:37.575
 Thread[pool-1-thread-1,5,main], job1@21:59:57.580
  并非每五秒启动下一个任务,而是每隔20 秒,原来是只有一个线程来执行所有任务,后面的任务必须等前一个任务释放出了线程才能得到执行。我们可以理解为 Spring 在任务调度时,fixedRate, fixedDelay 或 cron 只是决定提交任务到线程池的时刻,至于真正执行任务的时间就看有没有空闲的线程,因此最终决定于线程池的配置。
 同样,如果我们在ScheduleRunner 中声明两个任务(后续的执行输出结果都以这两个任务为例)

    @Component 
    public class ScheduleRunner { 
     
     @Scheduled(fixedDelay = 5000) 
     public void job1() { 
     System.out.println(Thread.currentThread() + ", job1@" + LocalTime.now()); 
     try { 
     Thread.sleep(20000); 
     } catch (InterruptedException e) { 
     } 
     } 
     
     @Scheduled(fixedDelay = 5000) 
     public void job2() { 
     System.out.println(Thread.currentThread() + ", job2@" + LocalTime.now()); 
     } 
    }

执行的效果是下面那样的
Thread[pool-1-thread-1,5,main], job2@22:05:12.236
Thread[pool-1-thread-1,5,main], job1@22:05:12.241
Thread[pool-1-thread-1,5,main], job2@22:05:32.244
Thread[pool-1-thread-1,5,main], job1@22:05:37.246
Thread[pool-1-thread-1,5,main], job2@22:05:57.250
Thread[pool-1-thread-1,5,main], job1@22:06:02.253
也是因为始终只有一个线程的缘故,任务调度无法按照预定的要求,job1 和 job2 不能同时进行,更别说 job1 或是 job2 的前后两次任务同时进行。job2 每次要等待 job1 执行完释放出线程来执行,所以不管 fixedDelay 或 fixedRate 配置多小的时间间隔,中间都至少要等 20 秒。
既然我们知晓了是单一线程的原因,那么再追根究底看看,以及解决办法是什么?
如何创建任务线程的?
查看源代码是最有效的,采用顺藤摸瓜的办法,从 @EnableScheduling 起,在 EnableScheduling 中找到 @see ScheduledAnnotationBeanPostProcessor, 来到ScheduledAnnotationBeanPostProcessor.setScheduler(Object scheduler)方法的 JavaDoc
说的是定时任务需要一个线程池(TaskScheduler 或 ScheduledExecutorService) 来执行,Spring 会通过以下顺序去获得 TaskScheduler 或是 ScheduledExecutorService 包装为 TaskScheduler 实例
1、类型为 TaskScheduler 的唯一 Bean
2、如果第 1 步未找到,或找到多个就尝试查找名称为 "taskScheduler", 类型为 TaskScheduler 的 Bean
3、查找类型为 ScheduledExecutorService 的 Bean, 并包装为 TaskScheduler 实例
4、如果第 3 步未到,或找到多个就尝试查找 名称为"taskScheduler", 类型为 ScheduledExecutorService 的 Bean, 并包装为 TaskScheduler 实例

也就是可以定一唯的类型为 TaskScheduler 或 ScheduledExecutorService 的 Bean, 或者是名称为 "taskScheduler" 的 TaskScheduler 或 ScheduledExecutorService 实例。

查找 TaskScheduler 的方法是ScheduledAnnotationBeanPostProcessor.finishRegistration(), 点接该链接查看源代码。
找到了 TaskScheduler 或 ScheduledExecutorService 后设置 Scheduler 的代码如下,在ScheduledTaskRegistrar类中

     public void setScheduler(Object scheduler) { 
    Assert.notNull(scheduler, "Scheduler object must not be null"); 
    if (scheduler instanceof TaskScheduler) { 
    this.taskScheduler = (TaskScheduler) scheduler; 
    } 
    else if (scheduler instanceof ScheduledExecutorService) { 
    this.taskScheduler = new ConcurrentTaskScheduler(((ScheduledExecutorService) scheduler)); 
    } 
    else { 
    throw new IllegalArgumentException("Unsupported scheduler type: " + scheduler.getClass()); 
    } 
    } 

对 ScheduledExecutorService 的包装是通过 ConsurrentTaskScheduler 类。
而在 ScheduledTaskRegistrar 中注册任务是由 scheduleTasks() 实现的,

     protected void scheduleTasks() { 
    if (this.taskScheduler == null) { 
    this.localExecutor = Executors.newSingleThreadScheduledExecutor(); 
    this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor); 
    } 
    ...... 
    } 

这才看到为什么默认情况下 Spring 用单线程来执行所有的任务, 因为 Spring 未定义 TaskScheduler 和 ScheduledExecutorService 这两个实例。此名,上面的
Executors.newSingleThreadScheduledExecutor()
最终会调用 Executors.defaultThreadFactory() 来创建 daemon 为 false 的线程。

  • 四、提供自定义的任务线程池

一般来说,只用一个线程来执行所有的任务是满足不了我们的需求的,除非项目中只有一个任务时的以下两种情况
• 用 fixedDelay 来配置的
• fixedRate 或 cron, 并且在时间间隔内每次任务必须能执行完成
知道了来龙去脉,就可以参考上面 1, 2, 3, 4 的顺序来定义一个自己的 TaskScheduler 来 ScheduledExecutorService 实例
• 类型为 TaskScheduler 或 ScheduledExecutorService 的实例
• 名称为 "taskScheduler" 的 TaskScheduler 或 ScheduledExecutorService 实例
TaskScheduler 接口有三个实现,分别是 ThreadPoolTaskScheduler,
ConcurrentTaskScheduler, 和 DefaultMangedTaskScheduler(继承自 ConsurrentTaskScheduler)
ScheduledExecutorService 接口有两个实现类,分别是 ScheduledThreadPoolExecutor
DelegatedScheduledExecutorService
下面是几个例子,可在前面的 Application 类中配置一个 @Bean, 代码如下

     @Bean 
    public TaskScheduler taskScheduler() { 
    ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); 
    taskScheduler.setPoolSize(5); 
    return taskScheduler; 
    }   

再次运行

Thread[taskScheduler-1,5,main], job2@23:21:09.307

Thread[taskScheduler-2,5,main], job1@23:21:09.307

Thread[taskScheduler-1,5,main], job2@23:21:14.315

Thread[taskScheduler-3,5,main], job2@23:21:19.318

Thread[taskScheduler-1,5,main], job2@23:21:24.322

Thread[taskScheduler-1,5,main], job2@23:21:29.326

Thread[taskScheduler-2,5,main], job1@23:21:34.320

Thread[taskScheduler-4,5,main], job2@23:21:34.327



现在分别由不同的的线程来执行各自的任务,互不干涉,每次任务由谁来执行只取决于池中的空闲线程。现在终于是 job1 每 25(20+5) 秒, job2 每 5 秒执行一次。应用中应根据任务间隔与每个任务执行时长来配置线程池的大小。此时线程池的名称是 TaskScheduler Bean 的名称,所以我们想改变线程池名称的话可以命一个新的 Bean 名称,改方法名或是指定 @Bean 的 name 属性,如

    @Bean(name = "TaskPool") 
    public TaskScheduler taskScheduler() { 
    ..... 
    } 


那么执行后打印的线程名称是

Thread[TaskPool-2,5,main], job1@23:26:09.330

Thread[TaskPool-1,5,main], job2@23:26:09.330

线程 daemon 应该是 false, 除非主线程自己不退

注意,如果是自己定义的线程池不能把线程的 daemon 设置为 true, 否则主线程很快退出进而整个进程结束,那就不是定时任务了。例如我们声明如下的 taskScheduler

     @Bean 
    public TaskScheduler taskScheduler() { 
    AtomicInteger number = new AtomicInteger(1); 
    ConcurrentTaskScheduler taskScheduler = new ConcurrentTaskScheduler( 
    Executors.newScheduledThreadPool(3, r -> { 
    Thread thread = new Thread(r); 
    thread.setName("TaskPool-thread-" + number.getAndIncrement()); 
    thread.setDaemon(true); //daemon 为 true 导致主线程很快退出,从而进程退出 
    return thread; 
    })); 
    return taskScheduler; 
    } 


执行程序后的效果可能是这样的

这还比较幸运,任务被执行了一次,进程退出了,也有可能一次任务都无法执行,如果是 fixedDelay 稍长的任务更是不可能得到一次执行的机会进程就退出了。如果你的主线程自己控制了永不退出也是可行的。

这种情况下,我们一般是不会这么干 -- 把线程的 daemon 设置为 true,这也就是为什么 ConcurrentTaskScheduler 接收的是一个 ScheduledExecutorService 参数。

名称 "taskScheduler" 或类型 "ScheduledExecutorService" 来查找相应的 Bean, 如果都没有找到,就会使用默认的单线程的 scheduler 来 执行任务,这就是我们之前看到的效果。

@Scheduled 与 @Async
还是有必要提到一种情况,@Scheduled 和 @Async 是可以共存的。可以试着这么做
• 给 Application 类加上 @EnableAsync
• 给 ScheduleRunner 的 job1() 和 job2() 方法加上注解 @Async
执行后

Thread[SimpleAsyncTaskExecutor-1,5,main], job1@00:13:36.763

Thread[SimpleAsyncTaskExecutor-2,5,main], job2@00:13:36.763

Thread[SimpleAsyncTaskExecutor-3,5,main], job1@00:13:41.738

Thread[SimpleAsyncTaskExecutor-4,5,main], job2@00:13:41.738

Thread[SimpleAsyncTaskExecutor-5,5,main], job1@00:13:46.742

Thread[SimpleAsyncTaskExecutor-6,5,main], job2@00:13:46.742

SimpleAsyncTaskExecutor 并不使用线程池来执行任务,而是每次创建新的线程来执行任务,由于 job1() 和 job2() 两方法是异步的,所以 fixedDelay 的效果与 fixedRate 是一样的,因为方法一调用即认为是结束,马上就安排下一次执行的时间。如果想用 fixedDelay 让前后两次任务是有关联的,方法不能为 @Async.



给自己备注一下:

用 @Scheduled 标注的方法最后是包装到ScheduledMethodRunnable 中被执行的,它是一个 Runnable 接口的实现

Runnable runnable = new ScheduledMethodRunnable(bean, invocableMethod);

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,408评论 4 371
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,690评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,036评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,726评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,123评论 3 296
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,037评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,178评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,964评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,703评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,863评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,333评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,658评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,374评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,195评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,988评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,167评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,970评论 2 279

推荐阅读更多精彩内容

  • 用过 Spring 的 @EnableScheduling 的都知道,我们用三种形式来部署计划任务,即 @Sche...
    mlion阅读 1,887评论 0 0
  • 用SpringBoot做定时任务只需要添加相应的注解就okay,相当简单。 1.在启动类上添加@EnableSch...
    忧郁的小码仔阅读 686评论 0 0
  • 20180611-20180617文|过云雨 壹|结伴同行 对于运动、看书、学习、写作来说,最好就是有一个小伙伴,...
    过云雨Milo阅读 147评论 0 1
  • 今天,旦旦有点流鼻涕。早上送去幼儿园,回来后,鼻涕多了一些。应该是感冒了。 晚上,旦妈去上晚自习,要三个小时。旦旦...
    装蝴蝶朱阅读 325评论 0 2
  • 也许,没有也许。可能,不再可能。遗忘,终将遗忘。我不可能等你那么久,如果等不到,我就放手了,亲爱的,我等不了了
    落叶晓枫阅读 199评论 0 0