收藏夹吃灰系列:谁说Spring提供的@Scheduled定时不好用?师妹看了直呼叫好!

Posted bug菌√

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了收藏夹吃灰系列:谁说Spring提供的@Scheduled定时不好用?师妹看了直呼叫好!相关的知识,希望对你有一定的参考价值。

 每周一更的收藏夹吃灰系列又一度提上日程,今晚分享点啥好嘞,哦嚯,有主意了,那还请小伙伴们移目下方吧,"线程池启动执行定时任务"新鲜出炉啦,赶紧趁热吃。

本文背景:

       最近接手了一老项目,搁置了有一段时间,现在要我进行项目迭代,也就是二次开发,然后有遇到这么个问题,"就是在项目中设置了定时任务之后 , 发现存在多个定时(周期)任务中总是有个别任务不执行",经过一番折腾之后 , 原来是spring提供的定时器任务scheduled-tasks默认配置是单线程串行执行的。就是说:即在当前时间点之内,如果同时有两个定时任务需要执行的时候, 排在第二个的任务就必须等待第一个任务执行完毕执行才能正常运行。如果第一个任务耗时较久的话 , 就会造成第二个任务不能及时执行。这样就很可能由于任务执行失败导致效性带来一系列问题,后果可想而知,而在实际项目中 , 我们也往往希望这些定时任务是"各干各的" , 而不是排队执行,做到如何并行化?这个问题想必大家都已经浮现在脑海中了。

       有小伙伴可能立马就想到了@Async@EnableAsync,采用异步的方式去处理不就好了,配置不同的线程去执行,思路是对的,但还是略微带点瑕疵,反问:"假如任务A的任务执行时间大于任务调度周期时间的话",会发生什么?很有可能上一个任务未执行完,下一个任务就又开始执行,这种风险带来的影响也很大吧,首先就是定时任务时效性问题,大家说这个怎么规避,显然不是很好,那么有比较适合解决该任务串行化的好方案么,阿柴:"怎么会没有,铁子接着往下看"。

       所以最好的方式就是采用多线程,自己控制线程池的数量,有几个任务控制起几个线程,使每个定时任务都在不同的线程上执行,彼此的执行次序和执行时间也都互不影响。

       那么阿柴,怎么实现呢?这是个大问题......

       接下来,柴兄弟就来教你,学不废请顺着网线来捶我好伐!今晚就算是熬通宵也得把你们教废咯,包教包会,有手就行。

如果觉得这篇文章对你们有所帮助,还请不忘在文章的左下角,直接pia的一下点亮它 up up up!!!若是我,不用犹豫直接进我的收藏夹吃灰去吧!不管以后用不用的上,先吃上灰再说,哈哈哈哈哈嗝~

 1、线程池配置:

SchedulingConfig.java

package com.system.xiaoma.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;


/**
 * 使用自定义的线程池执行异步任务 , 并设置定时任务的异步处理
 * @author luoyong
 * @since 2021-05-15
 */
@Slf4j
@Configuration
@EnableScheduling
@ConditionalOnProperty(prefix = "scheduling", name = "enabled", havingValue = "true")
public class SchedulingConfig implements SchedulingConfigurer, AsyncConfigurer {


    /**
     * 使用线程池执行定时任务
     *
     * @param taskRegistrar 注册器
     */
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        // 获取线程池
        TaskScheduler taskScheduler = taskScheduler();
        // 设置执行定时任务的线程池
        taskRegistrar.setTaskScheduler(taskScheduler);
    }

    /**
     * 配置一个线程池用于执行定时任务
     */
    @Bean(destroyMethod = "shutdown")
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5);
        scheduler.setThreadNamePrefix("server-assisted-guard-");
        scheduler.setAwaitTerminationSeconds(60);
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        return scheduler;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (throwable, method, objects) -> {
            log.error("异步任务执行出现异常, message {}, method {}, params {}", throwable, method, objects);
        };
    }


}

 2、定时配置

application.properties

###定时器开关 true启动/false关闭定时任务
scheduling.enabled=true

###清除日志定时任务时间配置,默认值每个五分钟执行一次
xiaoma.log.clear-task-cron=0 0/5 * * * ? 
# 日志保留时间,单位天,默认值30
xiaoma.log.log-timeout=30

3、实例演示:

LogInfoServiceImpl.java

/**
 * 定时任务清除日志表3个月前的数据
 * 演示:每五分钟执行一次
 * 注意: 定时方法不需要任何参数,并且返回类型为 void
 */
@Scheduled(cron = "${xiaoma.log.clear-task-cron:0 0/5 * * * ? }")
public void clear() {
    log.info("开始执行日志清理定时任务");
    // 获取当前时间
    Calendar calendar = Calendar.getInstance();
    // 当前时间减去日志保留时间
    calendar.add(Calendar.DAY_OF_MONTH, -logTimeOut);
    logInfoMapper.clear(calendar.getTime());
    log.info("日志清理定时任务执行成功");
}

LogInfoMapper.java

/**
 * 清除指定日期之前的日志
 *
 * @param date 时间
 */
void clear(Date date);

LogInfoMapper.xml

<!-- 清除日志 -->
<delete id="clear">
    <![CDATA[
    delete from log_info where create_time<#{date}
    ]]>
</delete>

于是在SpringBoot中自定义线程池,执行异步任务 , 并设置定时任务的异步处理;

我设置的是时隔五分钟,静待五分钟;

看下控制台输出打印;好家伙,是我想要看到的结果。

总结: 定时删除日志任务肯定是起效了;

但是阿柴啊,这也就起一定时任务啊,谁不会这啊,任务并行执行呢?......

很多小伙伴看到这肯定有些不耐烦了,别急别急,好戏还在后头呢,接着往下看~


那接下来,就再做个试验:验证启动两个在同一节点的定时任务(clear1和clear2),看看两定时任务到底是异步执行还是串行执行?

ps:由于是测试,就设置两任务都按每一分钟执行一次吧。

预期效果,应该两任务都同时打印定时任务开始,随后再执行定时任务结束!这逻辑才是对的,静候佳音~

@Scheduled(cron = "${xiaoma.log.clear-task-cron:0 0/1 * * * ? }")
public void clear1() {
    log.info("定时任务一开始");
    // 获取当前时间
    Calendar calendar = Calendar.getInstance();
    // 当前时间减去日志保留时间
    calendar.add(Calendar.DAY_OF_MONTH, -logTimeOut);
    logInfoMapper.clear(calendar.getTime());
    log.info("定时任务一结束");
}

@Scheduled(cron = "${xiaoma.log.clear-task-cron:0 0/1 * * * ? }")
public void clear2() {
    log.info("定时任务二开始");
    // 获取当前时间
    Calendar calendar = Calendar.getInstance();
    // 当前时间减去日志保留时间
    calendar.add(Calendar.DAY_OF_MONTH, -logTimeOut);
    logInfoMapper.clear(calendar.getTime());
    log.info("定时任务二结束");
}

接下来,就是见证奇迹的时候啦!噔噔噔~~

总结:通过控制台打印信息可以看到每个定时任务都在不同的线程上执行,彼此的执行次序和执行时间也互不影响,说明配置生效了,成功异步执行。

另外,集群版本如果要使用@Scheduled的话,需要加分布式锁来控制,或者直接用分布式定时任务Elasticjob或者xxl-job等。


热文推荐:

收藏夹吃灰系列(一):你一定没用过的代码生成工具,好不好用你们说了算

收藏夹吃灰系列(二):教小学妹如何通过代码实现Swagger在线接口文档转word文档!教完后...

收藏夹吃灰系列(三):你一定没用过的MySQL数据库表结构文档生成器,用完后相信你一定会点赞三连的!


 为了帮助更多小白从零打造java开发工程师,特地从CSDN官方那讨来了一套 《Java 工程师学习成长知识图谱》

官方出品必属精品!有兴趣的小伙伴可以了解一下!特价优惠,限时限量,抓紧时间哦~


附:cron规则在线教学

1、表达式:{秒数} {分钟} {小时} {日期} {月份} {星期} {年份(可为空)}

2、占位符解释:

字段

允许值

允许的特殊字符

秒数

0~59

- , * /

分钟

0~59

- , * /

小时

0~23

- , * /

日期

1~31,需注意每个月的天数

- , * / ? L W C

月份

1~12或JAN-DEC

- , * /

星期

1~7或SUN-SAT

- , * / ? L C #

年份

1970~2099,可为空

- , * /

星期:1代表星期天(一星期的第一天),7代表星期六(一星期的最后一天)。

3、特殊字符:

  • -代表在指定的范围内触发,比如"25-45"代表从25秒开始触发到45秒结束触发,每隔1秒触发1次
  • ,代表在指定的秒数触发,比如"0,15,45"代表0秒、15秒和45秒时触发任务
  • *代表每隔1秒钟触发
  • /代表触发步进(step),斜杠前面的值代表初始值,后面的值代表偏移量
    • "0/20"或"/20":从0秒钟开始,每隔20秒钟触发1次,即0秒触发1次,20秒触发1次,40秒触发1次
    • "5/20":5秒触发1次,25秒触发1次,45秒触发1次
    • "10-45/20":在[10,45]内步进20秒命中的时间点触发,即10秒触发1次,30秒触发1次
  • ?代表日期和星期互斥,即把日期占位符标为问好则代表是指定星期触发,把星期占位符标为问好则代表是指定日期触发
  • L(Last)若用在日期上代表这个月最后一天;若用在星期上代表这周的最后一天即星期六;若用在星期上并且配合数字如"5L"代表这个月的最后一个星期四触发
  • W(Weekday)只用在日期上,代表最接近指定天的工作日(周一到周五),比如"15W":最接近这个月第15天的工作日
    • 如果这个月第15天是周六,那么触发器将会在这个月第14天即周五触发
    • 如果这个月第15天是周日,那么触发器将会在这个月第16天即周一触发
    • 如果这个月第15天是周二,那么就在触发器这天触发
    • 只会在当前月计算值,不会越过当前月
    • "LW"代表这个月的最后一个工作日
  • #只用在星期上,代表这个月的第几个周几,比如"6#3"指这个月第3个周五(6指周五,3指第3个),如果指定的日期不存在,触发器就不会触发
  • C(Calendar)代表依靠一个指定的“日历”,没有“日历”关联,则等价于所有包含的“日历”
    • 用在日期上"5C"表示关联“日历”中第一天,或者这个月开始的第一天的后5天
    • 用在星期上"1C"表示关联“日历”中第一天,或者星期的第一天的后1天,也就是周日的后一天(周一)

4、举例:

  • "0 * 21-23,0-5 * * ?" 晚上9点到第二天凌晨5点,每一分钟执行一次(跨天执行)
  • "0 * 17-23/2,8 * * ?" 17点到23点每隔两小时和上午8点,每一分钟执行一次
  • "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触发

如果文章对您有所帮助,就请在文章末尾的左下角把大拇指点亮吧!(#^.^#);

如果喜欢阿柴柴(笔名)分享的文章,就请给阿柴柴个关注吧!(๑′ᴗ‵๑)づ╭❤~;

对文章有任何问题欢迎小伙伴们下方留言或者入群探讨【群号:708072830】;

鉴于个人经验有限,所有观点及技术研点,如有异议,请直接回复参与讨论(请勿发表攻击言论,谢谢);

版权声明:本文为博主原创文章,转载请附上原文出处链接和本文声明,版权所有,盗版必究!(*^▽^*).

以上是关于收藏夹吃灰系列:谁说Spring提供的@Scheduled定时不好用?师妹看了直呼叫好!的主要内容,如果未能解决你的问题,请参考以下文章

首次公开,整理12年积累的博客收藏夹,零距离展示《收藏夹吃灰》系列博客

收藏夹吃灰系列:Springboot配置Thymeleaf实现静态页面访问 | 超级详细,建议收藏!

收藏夹吃灰系列:Springboot配置Thymeleaf实现静态页面访问 | 超级详细,建议收藏!

收藏夹吃灰系列(十四):Java8 StreamAPI的详细用法 | 开箱即用,超级详细。

收藏夹吃灰系列(十四):Java8 StreamAPI的详细用法 | 开箱即用,超级详细。

收藏夹吃灰系列(十四):Java8 StreamAPI的详细用法 | 开箱即用,超级详细。