文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

一文带你吃透定时任务框架

2024-11-29 18:56

关注

一、介绍

说到定时任务,相信大家都不陌生,在我们实际的工作中,用到定时任务的场景可以说非常的多,例如:

等等,定时器像水和空气一般,普遍存在于各个场景中,在实际的业务开发中,基本上少不了定时任务的应用。

总结起来,一般定时任务的表现有以下几个特征:

在某个时刻触发,例如11.11号0点开启秒杀

按照固定频率周期性触发,例如每分钟发送心跳请求

预约指定时刻开始周期性触发,例如从12.1号开始每天7点闹钟响起

说了这么多,也不BB了,下面我们就来点干活,列举一些实际项目中使用的相关工具!

二、crontab 定时器

2.1、介绍

crontab 严格来说并不是属于 java 内的,它是 linux 自带的一个工具,可以周期性地执行某个shell脚本或命令。

由于 crontab 在实际开发中应用比较多, 特别是对于运维的人,crontab 命令是必须用到的命令,自动化运维中一定少不了它,而且 crontab 表达式跟我们后面要介绍的其他定时任务框架,例如 Quartz,Spring Schedule 的 cron 表达式类似,所以这里先介绍 crontab。

简而言之,crontab 就是一个自定义定时器。

命令格式如下:

.---------------- minute (0 - 59)
|  .------------- hour (0 - 23)
|  |  .---------- day of month (1 - 31)
|  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
|  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) ...
|  |  |  |  |
*  *  *  *  *  command

具体应用的时候,时间定义大概是长下面这个样子!

# 每5分钟执行一次命令
*/5 * * * * Command
# 每小时的第5分钟执行一次命令
5 * * * * Command
# 指定每天下午的 6:30 执行一次命令
30 18 * * * Command
# 指定每月8号的7:30分执行一次命令
30 7 8 * * Command
# 指定每年的6月8日5:30执行一次命令
30 5 8 6 * Command
# 指定每星期日的6:30执行一次命令
30 6 * * 0 Command

2.2、具体示例

以centOS操作系统为例,创建一个定时任务,每分钟执行某个指定shell脚本,过程如下!

yum -y install cronie yum-cron
#创建一个test.sh脚本
vim /root/shell/test.sh

#脚本内容如下,将内容输出到file.log文件
echo `date '+%Y-%m-%d %H:%M:%S'` >> /root/shell/file.log
sh /root/shell/test.sh
cat /root/shell/file.log

如果出现以下内容,说明运行正常!

图片

#编辑定时任务【删除-添加-修改】
crontab -e
#每分钟执行一次test.sh脚本
*/1 * * * * sh /root/shell/test.sh
#查看crontab定时任务
crontab -l

图片

##重新载入配置
systemctl reload crond
#重启服务
systemctl restart crond
tail -f /root/shell/file.log

从结果上看,运行正常!

图片

tail -f /var/log/cron

如果想移除某个定时任务,直接输入crontab -e命令,并移除对应的脚本,然后刷新配置、重启服务即可!

如果想深入的了解crontab使用,可以参考这篇文章,里面总结得比较好, 这里就不再多说了。

三、java 自带的定时器

3.1、Timer

Timer 定时器,由 jdk 提供的java.util.Timer和java.util.TimerTask两个类组合实现。

其中TimerTask表示某个具体任务,而Timer则是进行调度任务处理。

实现过程也很简单,示例如下:

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

public class TimerTest extends TimerTask {

    private String jobName;

    public TimerTest(String jobName) {
        this.jobName = jobName;
    }

    @Override
    public void run() {
        System.out.println("execute " + jobName);
    }

    public static void main(String[] args) {
        Timer timer = new Timer();
        long delay1 = 1 * 1000;
        long period1 = 1 * 1000;
        // 从现在开始 1 秒钟之后,每隔 1 秒钟执行一次 job1
        timer.schedule(new TimerTest("job1"), delay1, period1);

        long delay2 = 2 * 1000;
        long period2 = 2 * 1000;
        // 从现在开始 2 秒钟之后,每隔 2 秒钟执行一次 job2
        timer.schedule(new TimerTest("job2"), delay2, period2);
    }
}

输出结果:

execute job1
execute job2
execute job1
execute job1
execute job2
execute job1
execute job1
execute job2
...

Timer 的优点在于简单易用,由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。

具体原因如下:

基于上面的原因,Timer 现在生产环境中都不在使用!

3.2、ScheduledExecutor

鉴于 Timer 的上述缺陷,从 Java 5 开始,推出了基于线程池设计的 ScheduledExecutor。

其设计思想是,每一个被调度的任务都会由线程池中一个线程来管理执行,因此任务是并发执行的,相互之间不会受到干扰。需要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态。

实现过程,示例如下:

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

public class ScheduledExecutorTest implements Runnable {

    private String jobName;

    public ScheduledExecutorTest(String jobName) {
        this.jobName = jobName;
    }

    @Override
    public void run() {
        System.out.println("execute " + jobName);
    }

    public static void main(String[] args) {
        //设置10个核心线程
        ScheduledExecutorService service = Executors.newScheduledThreadPool(10);

        long initialDelay1 = 1;
        long period1 = 1;

        // 从现在开始1秒钟之后,每隔1秒钟执行一次job1
        service.scheduleAtFixedRate(
                new ScheduledExecutorTest("job1"), initialDelay1,
                period1, TimeUnit.SECONDS);

        long initialDelay2 = 2;
        long delay2 = 2;
        // 从现在开始2秒钟之后,每隔2秒钟执行一次job2
        service.scheduleWithFixedDelay(
                new ScheduledExecutorTest("job2"), initialDelay2,
                delay2, TimeUnit.SECONDS);
    }
}

输出结果:

execute job1
execute job1
execute job2
execute job1
execute job1
execute job2
execute job1

在 ScheduledExecutorService 中,由initialDelay、delay和TimeUnit三个参数决定任务的执行频率。

其中:

从 api 上可以看到,ScheduledExecutorService 的出现,完全可以替代 Timer  ,同时完美的解决上面所说的 Timer 存在的两个问题!

但是 ScheduledExecutorService 也不是万能的,比如我想每月1号统计一次报表、每季度月末统计销售额等等这样的需求。

你会发现使用 ScheduledExecutorService 实现的时候,每次任务执行之后,你需要从当前时间开始出下一次执行时间的间隔,而且每次都要重算,非常麻烦!

遇到这样的需求,就需要一个更加完善的任务调度框架来解决这些复杂的调度问题。

而我们所熟悉的开源框架 Quartz 在这方面就提供了强大的支持。

四、第三方定时器

4.1、Quartz

quartz 在 java 项目中应用非常的广,市面上很多的开源调度框架也基本都是直接或间接基于这个框架来开发的。

下面我们就通过一个例子,来简单地认识一下它。


    org.quartz-scheduler
    quartz
    2.3.2
public class QuartzTest implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
    }

    public static void main(String[] args) throws SchedulerException {
        // 创建一个Scheduler
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 启动Scheduler
        scheduler.start();

        // 新建一个Job, 指定执行类是QuartzTest, 指定一个K/V类型的数据, 指定job的name和group
        JobDetail job = JobBuilder.newJob(QuartzTest.class)
                .usingJobData("jobData", "test")
                .withIdentity("myJob", "myJobGroup")
                .build();

        // 新建一个Trigger, 表示JobDetail的调度计划, 这里的cron表达式是 每1秒执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("myTrigger", "myTriggerGroup")
                .startNow()
                .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?"))
                .build();

        // 让scheduler开始调度这个job, 按trigger指定的计划
        scheduler.scheduleJob(job, trigger);
    }
}

输出结果:

2020-11-09 21:38:40
2020-11-09 21:38:45
2020-11-09 21:38:50
2020-11-09 21:38:55
2020-11-09 21:39:00
2020-11-09 21:39:05
2020-11-09 21:39:10
...

当然,你还可以从JobExecutionContext对象中,获取上文usingJobData()方法中设置的值。

@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
    //从context中获取instName,groupName以及dataMap
    String jobName = context.getJobDetail().getKey().getName();
    String groupName = context.getJobDetail().getKey().getGroup();
    JobDataMap dataMap = context.getJobDetail().getJobDataMap();
    //从dataMap中获取myDescription,myValue以及myArray
    String value = dataMap.getString("jobData");
    System.out.println("jobName:" + jobName + ",groupName:" + groupName + ",jobData:" + value);
}

输出结果:

jobName:myJob,groupName:myJobGroup,jobData:test

在 Quartz 工具包中,设计的核心类主要包括 Scheduler, Job 以及 Trigger。

相比 JDK 提供的任务调度服务,Quartz 最明显的一个特点就是将任务调度者、任务具体实例、任务调度策略进行三方解耦,这么做的优点在于同一个 Job 可以绑定多个不同的 Trigger,同一个 Trigger 也可以调度多个 Job,配置灵活性非常强。

Trigger 同时还支持cron表达式,在任务调度时间配置方面,更加灵活。

当然,Quartz 的用途不仅仅在单例服务上,在分布式调度方面也同样应用非常广,由于篇幅原因,关于 Quartz 的详细使用介绍,我们会在后期的文章中详细深入分析。

4.2、Spring Schedule

与 Quartz 齐名的还有我们所熟悉的 Spring Schedule,由 Spring 原生提供支持。

实现上,Spring 中使用定时任务也非常简单。

4.2.1、基于 XML 配置
  

      
    

    
    

    
    
        
        
        
        
    

    
    

package com.spring.task;


public class TestTask {

    public void show() {
        System.out.println("show method 1");
    }

    public void print() {
        System.out.println("print method 1");
    }
}
4.2.2、基于注解配置

基于注解的配置,可以直接在方法上配置相应的调度策略,相比xml的方式更加简洁。

package com.spring.task;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;


@Component
public class TestTask2 {

    
    @Scheduled(cron = "*/5 * * * * ?")
    public void show() {
        System.out.println("show method 2");
    }

    
    @Scheduled(initialDelay = 60*1000, fixedRate = 10*1000)
    public void print() {
        System.out.println("print method 2");
    }
}
4.2.3、Spring Boot 定时任务应用

如果在 Spring Boot 项目中,使用就更加方便了。

@SpringBootApplication
@EnableScheduling
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
@Component
public class TestTask3 {

    private int count=0;

    @Scheduled(crnotallow="*/5 * * * * ?")
    private void process() {
        System.out.println("this is scheduler task running  "+(count++));
    }
}

输出结果:

this is scheduler task running  0
this is scheduler task running  1
this is scheduler task running  2
...
4.2.4、任务执行规则

最后,我们来看看 Spring Schedule 的任务执行规则,打开@Scheduled注解类,源码如下:

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {

    String cron() default "";

    String zone() default "";

    long fixedDelay() default -1;

    String fixedDelayString() default "";

    long fixedRate() default -1;

    String fixedRateString() default "";

    long initialDelay() default -1;

    String initialDelayString() default "";
}

从方法上可以看出,@Scheduled注解中可以传 8 种参数:

其中用的最多的就是cron表达式,下面我们就一起来看看如何来编写配置。

4.2.5、Cron 表达式

Spring 的 cron 表达式和 Quartz 的 cron 表达式基本都是通用的,但是与 linux 下 crontab 的 cron 表达式是有一定区别的,它可以直接到秒级别。

cron 表达式结构:

.---------------------- seconds(0 - 59)
|  .------------------- minute (0 - 59)
|  |  .---------------- hour (0 - 23)
|  |  |  .------------- day of month (1 - 31)
|  |  |  |  .---------- month (1 - 12)
|  |  |  |  |  .------- Day-of-Week (1 - 7) 
|  |  |  |  |  |  .---- year (1970 - 2099) ...
|  |  |  |  |  |  |
*  *  *  *  *  ?  *

具体样例如下:

图片

在 cron 表达式中不区分大小写,更多配置可以参考这里。

还可以在线解析cron表达式进行测试。

图片

4.3、elastic-job

elastic-job 是当当基于 quartz 二次开发而开源的一个分布式弹性作业框架,功能十分强大。

主要功能:

由于 elastic-job 是基于 Zookeeper 实现集群调度,因此在使用它之前,需要先搭建好 Zookeeper 服务器,网上教程很多,在此不过多介绍。

elastic-job 具体简单实现过程如下!


    
    
        com.dangdang
        elastic-job-lite-core
        2.0.5
    
    
    
        com.dangdang
        elastic-job-lite-spring
        2.0.5
    
    
        org.apache.zookeeper
        zookeeper
        3.4.9
    
public class MyElasticJob implements SimpleJob {

    public void execute(ShardingContext context) {
        System.out.println(context.toString());
        switch (context.getShardingItem()) {
            case 0:
                System.out.println("------------->>>>0");
                break;
            case 1:
                System.out.println("------------->>>>1");
                break;
            case 2:
                System.out.println("------------->>>>2");
                break;
            default:
                System.out.println("------------->>>>default");
                break;
        }
    }


    public static void main(String[] args) {
        new JobScheduler(createRegistryCenter(), createJobConfiguration()).init();
    }

    private static CoordinatorRegistryCenter createRegistryCenter() {
        //配置ZK注册中心地址
        CoordinatorRegistryCenter regCenter = new ZookeeperRegistryCenter(new ZookeeperConfiguration("10.211.108.160:2181", "elastic-job-demo"));
        regCenter.init();
        return regCenter;
    }
    private static LiteJobConfiguration createJobConfiguration() {
        JobCoreConfiguration simpleCoreConfig = JobCoreConfiguration.newBuilder("demoSimpleJob", "0/15 * * * * ?", 10).build();
        // 定义SIMPLE类型配置
        SimpleJobConfiguration simpleJobConfig = new SimpleJobConfiguration(simpleCoreConfig, MyElasticJob.class.getCanonicalName());
        // 定义Lite作业根配置
        LiteJobConfiguration simpleJobRootConfig = LiteJobConfiguration.newBuilder(simpleJobConfig).build();
        return simpleJobRootConfig;
    }
}

具体详细使用,可以参考官方网站。

4.4、xxl-job

xxl-job 是被广泛使用的另外一款使用的分布式任务调度框架,出自大众点评许雪里(xxl 就是作者名字的拼音首字母)的开源项目,官网上介绍这是一个轻量级分布式任务调度框架,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。

图片

跟elasticjob不同,xxl-job 环境依赖于mysql,不用 ZooKeeper,这也是最大的不同。早起的 xxljob 也是基于 quartz 开发的,不过现在慢慢去 quartz 化了,改成自研的调度模块。

xxl-job 具体简单实现过程如下!


    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.springframework.boot
        spring-boot-starter
    
    
        com.xuxueli
        xxl-job-core
        2.2.0
    
# web port
server.port=8081
# log config
logging.cnotallow=classpath:logback.xml
spring.application.name=xxljob-demo
### 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### 执行器通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=xxl-job-demo
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”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.logretentinotallow=10
@Configuration
public class XxlJobConfig {
    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;
    @Value("${xxl.job.accessToken}")
    private String accessToken;
    @Value("${xxl.job.executor.appname}")
    private String appname;
    @Value("${xxl.job.executor.address}")
    private String address;
    @Value("${xxl.job.executor.ip}")
    private String ip;
    @Value("${xxl.job.executor.port}")
    private int port;
    @Value("${xxl.job.executor.logpath}")
    private String logPath;
    @Value("${xxl.job.executor.logretentiondays}")
    private int logRetentionDays;

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        xxlJobSpringExecutor.setAddress(address);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
        return xxlJobSpringExecutor;
    }
}
@Component
public class XxlJobDemoHandler {
    
    @XxlJob("demoJobHandler")
    public ReturnT demoJobHandler(String param) throws Exception {
        XxlJobLogger.log("java, Hello World~~~");
        XxlJobLogger.log("param:" + param);
        return ReturnT.SUCCESS;
    }
}

写完之后启动服务,然后可以打开管理界面,找到执行器管理,添加执行器。

图片

接着到任务管理,添加任务。

最后我们可以到任务管理去测试一下,运行demoJobHandler。

图片

图片

点击保存后,会立即执行。点击查看日志,可以看到任务执行的历史日志记录

图片

这就是简单的Demo演示,具体详细使用,可以参考官方网站。

五、总结

本文主要围绕定时调度器的理论知识和用法做了一次知识的总结,如果有理解不对的地方,欢迎大家留言指出。

六、参考

https://juejin.im/post/6844904002606350343

https://developer.ibm.com/zh/languages/java/articles/j-lo-taskschedule/

https://www.cnblogs.com/linjiqin/p/11720673.html

https://www.cnkirito.moe/timer/

https://cloud.tencent.com/developer/article/1138669

https://developer.aliyun.com/article/775305

来源:Java极客技术内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯