定时任务调度方案

随着系统规模的发展,项目的组织结构以及架构越来越复杂,业务覆盖的范围越来越广,定时任务数量日益增多,任务也变得越来越复杂,尤其是为了满足在用户体量日历增大时,系统能够稳定运行,我们往往会扩充服务器做集群,无论是传统垂直项目还是如今主流的分布式。那么对定时任务的要求也逐渐变高,基于现在项目主流架构,定时任务需满足一下要求:

  1. 任务统一管理,提供图形化界面对任务进行配置和调度。
  2. 保证任务调度的幂等性(任务并发控制,同一个任务在同一时间只能允许一个执行)
  3. 任务弹性扩容,可根据繁忙情况动态增减服务器分摊压力,对大任务进行分片处理。
  4. 任务依赖问题,能够处理任务包含子任务的情况,前一个完成后触发子任务执行。
  5. 支持多类型的任务,支持Spring Bean、Shell等。
  6. 任务节点高可用,任务节点异常或者繁忙时能够转移到其他节点执行。
  7. 调度中心高可用,支持集群部署,避免出现单点故障。
  8. 执行状态监控,方便查看任务执行状态,异常情况告警,支持多渠道通知。

#. 发展史:

定时任务随着技术发展,从单线程调度到多线程调度,从单机部署到集群部署,从独立执行到多任务协同执行。

调度计划架构图 调度的工作计划怎么写_调度计划架构图

1. Thread

通过线程休眠实现,由JDK提供

private static int count = 0;

public static void main(String[] args) {
  Runnable runnable = new Runnable() {
    @Override
    public void run() {
      while (count < 8) {
        try {
          Thread.sleep(1000);
          System.out.printf("执行第:%d 次\n", ++count);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }
  };
  new Thread(runnable).start();
}

调度计划架构图 调度的工作计划怎么写_XXL-JOB_02

2. 线程池

有延缓执行实现,由JDK提供

private static int count = 0;

public static void main(String[] args) throws InterruptedException {
  for (int i = 0; i < 8; i++) {
    ExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
    Runnable runnable = new Runnable() {
      @Override
      public void run() {
        ++count;
        System.out.printf("执行从 %d 次\n", count);
        if (count > 7) {
          System.exit(0);
        }
      }
    };
    // 也可以使用 ScheduledExecutorService.schedule
    executorService.awaitTermination(1L, TimeUnit.SECONDS);
    executorService.execute(runnable);
  }
}

调度计划架构图 调度的工作计划怎么写_分布式任务调度_03

3. Timer TimeTask

由JDK提供,多用于移动端(如安卓开发)

private static int count = 0;

public static void main(String[] args) {
    TimerTask timerTask = new TimerTask() {
        @Override
        public void run() {
            System.out.printf("执行第 %d 次\n", ++count);
            if (count > 7) {
                System.exit(0);
            }
        }
    };
    Timer timer = new Timer();
    timer.schedule(timerTask, 500, 500);
}

调度计划架构图 调度的工作计划怎么写_分布式定时任务_04

4. Schedule

由Spring提供, 配合@EnableScheduling 使用

@Component
public class SpringScheduleTest {

    private static int count;

    @Scheduled(cron = "0/1 * * * * ?")
    public void testSchedule() {
        System.out.printf("执行第 %d 次\n", ++count);
    }
}

cron 表达式:秒-分-时-日-月-周-年

5.quartz

有maven独立依赖,在SpringBoot2.0之后自带包

  1. 纯maven项目依赖
<!-- 核心包 -->
<dependency>
   <groupId>org.quartz-scheduler</groupId>
   <artifactId>quartz</artifactId>
   <version>2.3.0</version>
</dependency>

<!-- 工具包 -->
<dependency>
   <groupId>org.quartz-scheduler</groupId>
   <artifactId>quartz-jobs</artifactId>
   <version>2.3.0</version>
</dependency>
  1. SpringBoot2.0及上依赖
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
  1. 测试类 Test.java
public class Test {

    public static void main(String[] args) throws SchedulerException, ParseException {
        Scheduler factory = StdSchedulerFactory.getDefaultScheduler();
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put("tips", "去你大爷的");
        JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class)
                .withIdentity("testJob", "testJobGroup")
                .setJobData(jobDataMap).build();
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("testTrigger", "testTriggerGroup")
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5))
                .startNow().build();
        factory.scheduleJob(jobDetail, trigger);
        factory.start();
    }
}
  1. 创建任务类 QuartzJob.java
public class QuartzJob implements Job {
    public static int count;
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        JobDataMap jobDataMap = jobExecutionContext.getMergedJobDataMap();
        String tips = jobDataMap.getString("tips");
        System.out.printf("获取到参数:%s\n", tips);
        System.out.printf("执行第:%d 次\n", ++count);
    }
}

6. Xxl-job

XXL-JOB 是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

调度计划架构图 调度的工作计划怎么写_System_05


详细内容介绍,请查看:文档地址

  1. 创建库,并创建表,sql文件在项目目录下:xxl-job-master/doc/db/tables_xxl_job.sql
    注意:它的数据库名是 xxl_job, 如果不想创建新库,而是导入到现有的库中,需要进入sql文件中改名字。
  2. 调度计划架构图 调度的工作计划怎么写_XXL-JOB_06

  3. 运行项目:xxl-job-admin
  1. 项目结构如下:
  2. 调度计划架构图 调度的工作计划怎么写_分布式定时任务_07

  3. 修改mysql 连接配置如下:
  4. 调度计划架构图 调度的工作计划怎么写_分布式定时任务_08

  1. 运行起来后, 在浏览器访问:http://localhost:8080/xxl-job-admin

默认账号:admin 密码:123456

  1. 成功登录后进入调度中心
  2. 调度计划架构图 调度的工作计划怎么写_XXL-JOB_09

  3. 注册任务执行器,可以单独创建执行器服务,也可集成到现有项目服务中去
    以下为新建SpringBoot 项目为示例
  1. 创建SpringBoot 项目并添加依赖
<dependency>
  <groupId>com.xuxueli</groupId>
  <artifactId>xxl-job-core</artifactId>
  <version>2.3.1</version>
</dependency>
  1. 在resources目录下创建application.properties文件(yaml文件也一样,如果有就不用创建了)
# 执行器所在的服务的运行端口, 非执行器端口
server.port=8081


### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
# 注册地址, 这个地址其实就是部署或者运行完xxl-job-admin调度中心的浏览器访问地址
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin

### xxl-job, access token
xxl.job.accessToken=default_token

### xxl-job executor appname
### 执行器名称,需要配置到调度中心
xxl.job.executor.appname=xxl-job-executor
### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null
xxl.job.executor.address=
### xxl-job executor server-info
xxl.job.executor.ip=
# 这个端口是执行器端口, 不是这个项目server.port 端口,也不能一样,否则会出现端口占用等错误信息
# 也就是说,执行器项目部署或者运行起来会有两个端口,如果是用docker部署,注意两个端口都要映射到宿主机端口
xxl.job.executor.port=9999
### xxl-job executor log-path
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### xxl-job executor log-retention-days
xxl.job.executor.logretentiondays=30
  1. 创建配置类 XxlJobConfig.java
@Configuration
public class XxlJobConfig {

    @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() {
        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;
    }
}
  1. 创建任务 (Job),注意不同版本写法会有些不一样
@Component
public class SampleXxlJob {

    /**
     * 1、简单任务示例(Bean模式)
     */
    @XxlJob("demoJobHandler")
    public void demoJobHandler() throws Exception {
        XxlJobHelper.log("XXL-JOB, Hello World.");
        for (int i = 0; i < 5; i++) {
            XxlJobHelper.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
        XxlJobHelper.handleSuccess("XXL-JOB:执行成功");
    }
}
  1. 配置执行器
  1. 配置
  2. 成功
  1. 配置任务并执行
  1. 配置任务
  2. 调度计划架构图 调度的工作计划怎么写_System_10

  3. 执行一次
  4. 调度计划架构图 调度的工作计划怎么写_分布式定时任务_11


  5. 调度计划架构图 调度的工作计划怎么写_调度计划架构图_12

  6. 查看日志
  7. 调度计划架构图 调度的工作计划怎么写_分布式任务调度_13


  8. 调度计划架构图 调度的工作计划怎么写_分布式定时任务_14


  9. 调度计划架构图 调度的工作计划怎么写_分布式任务调度_15