1. 为什么需要动态定时任务

在传统开发中,定时任务通常采用硬编码方式写在代码里。比如用Spring自带的@Scheduled注解,或者直接在XML中配置Quartz的JobDetail和Trigger。这种方式在小型项目中勉强够用,但遇到以下场景就会捉襟见肘:

  • 任务执行频率需要频繁调整时,每次都要改代码重新部署
  • 不同环境(开发/测试/生产)需要不同的任务配置
  • 需要动态控制任务的启停状态
  • 想要实时查看任务执行情况

我在电商项目中就遇到过这样的痛点:促销活动需要临时增加库存同步任务,但每次修改都要走发布流程,严重影响运营效率。后来改用动态配置方案后,运营同学在后台点点鼠标就能创建新任务,开发效率提升明显。

2. 基础环境搭建

2.1 初始化SpringBoot项目

先用Spring Initializr创建一个基础项目,选择这些依赖:

  • Spring Web
  • Spring Data JPA
  • Quartz Scheduler

或者直接在pom.xml中添加:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>

2.2 数据库配置

Quartz需要一些表来存储任务信息,官方提供了建表SQL,在项目的resources目录下创建schema.sql:

CREATE TABLE qrtz_jobs (
  id bigint NOT NULL AUTO_INCREMENT,
  job_name varchar(200) NOT NULL,
  job_group varchar(200) NOT NULL,
  description varchar(250) DEFAULT NULL,
  job_class_name varchar(250) NOT NULL,
  is_durable varchar(1) NOT NULL,
  is_nonconcurrent varchar(1) NOT NULL,
  is_update_data varchar(1) NOT NULL,
  requests_recovery varchar(1) NOT NULL,
  job_data blob,
  PRIMARY KEY (id)
);
-- 其他表结构省略...

然后在application.yml中配置数据源和Quartz:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/quartz_demo
    username: root
    password: 123456
  quartz:
    job-store-type: jdbc
    properties:
      org.quartz.scheduler.instanceName: MyScheduler
      org.quartz.threadPool.threadCount: 5

3. 核心架构设计

3.1 任务元数据表设计

除了Quartz自带的表,我们需要自定义一张表来存储任务配置:

@Entity
public class ScheduleJob {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String jobName;
    private String jobGroup;
    private String cronExpression;
    private String status; // 0-暂停 1-运行
    private String className;
    private String methodName;
    // 其他字段...
}

3.2 任务抽象基类

定义一个抽象任务类,所有具体任务都继承它:

@DisallowConcurrentExecution
public abstract class AbstractJob implements Job {
    
    @Override
    public void execute(JobExecutionContext context) {
        long start = System.currentTimeMillis();
        try {
            doExecute(context);
        } catch (Exception e) {
            // 异常处理
        }
        long cost = System.currentTimeMillis() - start;
        // 记录执行日志
    }
    
    protected abstract void doExecute(JobExecutionContext context);
}

3.3 任务调度服务

核心调度服务类,封装了Quartz的API:

@Service
public class ScheduleService {
    
    @Autowired
    private Scheduler scheduler;
    
    public void addJob(ScheduleJob job) {
        // 构建JobDetail
        JobDetail jobDetail = JobBuilder.newJob(getJobClass(job.getClassName()))
                .withIdentity(job.getJobName(), job.getJobGroup())
                .build();
        
        // 构建Trigger
        CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(job.getJobName(), job.getJobGroup())
                .withSchedule(CronScheduleBuilder.cronSchedule(job.getCronExpression()))
                .build();
        
        // 调度任务
        scheduler.scheduleJob(jobDetail, trigger);
    }
    
    // 其他方法:暂停、恢复、删除等...
}

4. 动态任务管理实现

4.1 新增任务接口

@RestController
@RequestMapping("/schedule")
public class ScheduleController {
    
    @Autowired
    private ScheduleService scheduleService;
    
    @PostMapping("/add")
    public Result addJob(@RequestBody ScheduleJob job) {
        scheduleService.addJob(job);
        return Result.success();
    }
}

4.2 任务状态管理

public void pauseJob(String jobName, String jobGroup) {
    JobKey jobKey = new JobKey(jobName, jobGroup);
    try {
        scheduler.pauseJob(jobKey);
        // 更新数据库状态
    } catch (SchedulerException e) {
        throw new RuntimeException("暂停任务失败", e);
    }
}

4.3 表达式动态更新

public void updateJobCron(ScheduleJob job) {
    TriggerKey triggerKey = TriggerKey.triggerKey(job.getJobName(), job.getJobGroup());
    try {
        CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
        // 表达式有变化才更新
        if (!trigger.getCronExpression().equals(job.getCronExpression())) {
            trigger = trigger.getTriggerBuilder()
                    .withSchedule(CronScheduleBuilder.cronSchedule(job.getCronExpression()))
                    .build();
            scheduler.rescheduleJob(triggerKey, trigger);
        }
    } catch (SchedulerException e) {
        throw new RuntimeException("更新任务失败", e);
    }
}

5. 可视化界面集成

5.1 前端页面设计

用Vue+ElementUI实现管理界面,主要功能点:

  • 任务列表展示
  • 新增/编辑表单
  • 启停操作按钮
  • 执行日志查看

关键API接口:

  • GET /schedule/list - 获取任务列表
  • POST /schedule/add - 新增任务
  • PUT /schedule/update - 修改任务
  • POST /schedule/pause - 暂停任务

5.2 Cron表达式生成器

集成cron表达式可视化组件,让用户通过选择生成表达式:

import { Cron } from '@vue-js-cron/element-plus';

<template>
  <cron v-model="form.cronExpression" />
</template>

5.3 执行日志记录

在任务基类中增加日志记录逻辑:

protected void doExecute(JobExecutionContext context) {
    ScheduleJob job = getJobFromContext(context);
    JobLog log = new JobLog();
    log.setJobId(job.getId());
    log.setStartTime(new Date());
    
    try {
        // 执行实际业务逻辑
        executeBusiness(job);
        log.setStatus("SUCCESS");
    } catch (Exception e) {
        log.setStatus("FAIL");
        log.setErrorMsg(e.getMessage());
    } finally {
        log.setEndTime(new Date());
        jobLogRepository.save(log);
    }
}

6. 生产环境注意事项

6.1 集群部署方案

在集群环境下需要额外配置:

spring:
  quartz:
    properties:
      org.quartz.jobStore.isClustered: true
      org.quartz.jobStore.clusterCheckinInterval: 20000

6.2 任务幂等性处理

实现StatefulJob接口或使用@DisallowConcurrentExecution注解防止并发执行:

@DisallowConcurrentExecution
public class OrderSyncJob extends AbstractJob {
    // ...
}

6.3 失败重试机制

配置失败重试策略:

Trigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("trigger1", "group1")
        .startNow()
        .withSchedule(CronScheduleBuilder.cronSchedule("0 0/5 * * * ?")
                .withMisfireHandlingInstructionFireAndProceed())
        .build();

7. 踩坑经验分享

  1. 时区问题:生产环境发现任务执行时间不对,最后发现是服务器时区设置问题。解决方案是在启动参数添加:-Duser.timezone=GMT+08

  2. 事务问题:任务中需要添加@Transactional注解确保事务生效

  3. 内存泄漏:长时间运行后出现OOM,原因是JobDataMap没有清理。解决方案是定期调用scheduler.clear()

  4. 日志过大:高频任务产生大量日志,后来改用异步日志并设置合理的滚动策略

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐